diff --git a/README.md b/README.md index 8de397f..faf3905 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,11 @@ # Obsidian REPL plugin This plugin adds an emacs-like read evaluate print loop (REPL) to obsidian. -This lets you execute javascript in a document *and* importantly interact with editor objects as though you were writing a plugin. -This can be useful when developing plugins, or for "light-weight" scripting without having to develop a plugin. +This lets you execute javascript in a document *and* importantly interact with Obsidian's +plugin api to perform actions in Obsidian. + +This can be useful when developing plugins, or for "light-weight" scripting without having to develop a full plugin yourself. + +A range of convenience functions partly inspired my emacs is also provided. # Using Install the plugin. I would advise binding CTRL-J to the commamd (Execute the current line or selection). @@ -18,17 +22,50 @@ You might like to [refer to the plugin documentation](https://docs.obsidian.md/P ## Convenience functions provided Various convenience functions are provided: +* open(f:string) - Open a file in the current frame * dir(o:Object) - List the property in an object * message(s:string) - Print a message * command(s:string) - Run a command * lineNumber - return the line number of the current line * plugin(s:string) - Get the object for a plugin * bufferString() - Return a string containing the entire text of the buffer +* bufferString(start, end) - Return the string between these two cursor positions (editor.getCursor()) +* point() - Return the current cursor position +* mark() - Return the cursor position at the beginning of the selection * insert(s:string) - Insert a string into the buffer * pointMin() - Return the minimum cursor in the buffer * pointMax() - Return the maximun cursor in the buffer * forwardChar(count?) - Move count (or one) character forward +* writeToFile(name, string) - Overwrite the markdown file called name with the given string +* appendToFile(name, string) - Append to the markdown file called `name` with the given string +* selection() - Get the text contained in the selection +## Defining commands + +The function `makeNewCommand` will create a new command from a function. You can then +define a hotkey to this command. + +```js +makeNewCommand(function new_command_name() { +... +}) +``` + +creates a command with the name "new command name" (and the id `new_command_name`). +You can use all repl's extra functions and variables (`app`, `editor` etc) in this function. + +If you want to test this funtion by hand you can do the following + +```js +var f = makeNewCommand(function new_command_name() { +... +}) +``` + +You can then call f() using eval to test the function. + +# Running code at startup +If you want code to run at startup, such as for [defining commands](#commands) then you can place this code in a special file called `repl.md`. If this file exists it is read when obsidian starts (or is reloaded) and the code in it is executed. ## Importing modules I experimented with the [obsidian modules](https://github.com/polyipseity/obsidian-modules) plugin but had issues importing full modules. diff --git a/evalScope.js b/evalScope.js index d259618..9b757bc 100644 --- a/evalScope.js +++ b/evalScope.js @@ -1,17 +1,28 @@ function evalInScope (s, context) { - var result; + let _result; context["_s"] = s with (makeScope(context)) { if (/^ *\{/.test(_s)) { // object literal - result = eval("(" + _s + ")") + _result = eval("(" + _s + ")") } else { - result = eval(_s) + _result = eval(_s) } - return result + + return _result } } +function runInScope(f, context) { + let _result; + context["f"] = f + with (makeScope(context)) { + _result = f() + return _result + } +} + + function makeScope (target) { return new Proxy(target, { has(target, prop) { return true; }, @@ -21,3 +32,4 @@ function makeScope (target) { exports["evalInScope"] = evalInScope exports["makeScope"] = makeScope +exports["runInScope"] = runInScope diff --git a/main.ts b/main.ts index eb2d39f..f39bc22 100644 --- a/main.ts +++ b/main.ts @@ -5,6 +5,11 @@ import * as evalScope from "./evalScope" // Remember to rename these classes and interfaces! +type Cursor = { + line: number + ch: number +} + interface MyPluginSettings { mySetting: string; } @@ -13,6 +18,13 @@ const DEFAULT_SETTINGS: MyPluginSettings = { mySetting: 'default' } +function makeOpen(app: any) { + function open(name: string) { + app.workspace.openLinkText(name) + } + return open +} + function makePlugin(app: any) { function plugin(name: string) { return app.plugins.plugins[name] @@ -36,8 +48,14 @@ class Scope { eval(s: string) { return evalScope.evalInScope(s, this.context) } + + run(f: any) { + return evalScope.runInScope(f, this.context) + } } + + function dir(obj: any) { let p = []; for (; obj != null; obj = Object.getPrototypeOf(obj)) { @@ -61,6 +79,60 @@ function makeCommand(app: any) { return command } +function makeLineAtPoint(editor: Editor) { + function lineAtPoint() { + return editor.getLine(editor.getCursor().line) + } + return lineAtPoint +} + +function makeSelection(editor: Editor) { + function selection() { + let selection = editor.getSelection() + if (selection === "") { + throw Error("No text is selected") + } + return selection + } + return selection +} + + +function makeReadFile(app: any) { + const readFile = function(name: string) { + name = name + ".md" + const file = app.vault.getAbstractFileByPath(name) + return app.vault.read(file) + } + return readFile +} + + +function makeWriteToFile(app: any) { + const writeToFile = function(name: string, text: string) { + name = name + ".md" + const file = app.vault.getAbstractFileByPath(name) + return app.vault.modify(file, text) + } + return writeToFile +} + +function makeAppendToFile(app: any) { + const writeToFile = makeWriteToFile(app) + const readFile = makeReadFile(app) + async function appendToFile(name: string, appended: string) { + let existing = await readFile(name) + if (existing === undefined) { + existing = "" + } + const content = existing + appended + await writeToFile(name, content) + } + return appendToFile +} + + + function makeForwardChar(editor: Editor): (count: number | undefined) => void { function forwardChar(count: number | undefined): void { if (count === undefined) { @@ -92,17 +164,33 @@ function makePointMax(editor: Editor): () => EditorPosition { return pointMax } -function pointMin() { +function pointMin(): Cursor { return { line: 0, ch: 0 } } function makeBufferString(editor: Editor): () => string { - function bufferString() { - return editor.getRange(pointMin(), makePointMax(editor)()) + function bufferString(start?: Cursor, end?: Cursor) { + return editor.getRange(start || pointMin(), end || makePointMax(editor)()) } return bufferString } + +function makePoint(editor: Editor) { + function point(): Cursor { + return editor.getCursor("to") + } + return point +} + + +function makeMark(editor: Editor) { + function mark() { + return editor.getCursor("from") + } + return mark +} + function makeSaveExcursion(editor: Editor) { function saveExcursion(f: () => any): void { const point = editor.getCursor() @@ -151,36 +239,84 @@ const makeEndOfLine = (editor: any) => { editor.setCursor(cursor.line, line.length) } } + export default class ReplPlugin extends Plugin { settings: MyPluginSettings; scope: Scope history: History + initLoaded: boolean = false + + updateScopeApp() { + this.scope.add("repl", this) + this.scope.add("dir", dir) + this.scope.add("newCommand", this.makeNewCommand()) + this.scope.add("source", this.makeSource(this.app)) + this.scope.add("plugin", makePlugin(this.app)) + this.scope.add("command", makeCommand(this.app)) + this.scope.add("readFile", makeReadFile(this.app)) + this.scope.add("writeToFile", makeWriteToFile(this.app)) + this.scope.add("appendToFile", makeAppendToFile(this.app)) + this.scope.add("app", this.app) + this.scope.add("message", message) + this.scope.add("workspace", this.app.workspace) + this.scope.add("open", makeOpen(this.app)) + } + + updateScopeEditor(editor: Editor, view: MarkdownView) { + this.scope.add("endOfLine", makeEndOfLine(editor)) + this.scope.add("saveExcursion", makeSaveExcursion(editor)) + this.scope.add("lineNumber", makeLineNumber(editor)) + this.scope.add("lineAtPoint", makeLineAtPoint(editor)) + this.scope.add("bufferString", makeBufferString(editor)) + this.scope.add("point", makePoint(editor)) + this.scope.add("mark", makeMark(editor)) + this.scope.add("insert", makeInsert(editor)) + this.scope.add("pointMin", pointMin) + this.scope.add("pointMax", makePointMax(editor)) + this.scope.add("selection", makeSelection(editor)) + this.scope.add("forwardChar", makeForwardChar(editor)) + this.scope.add("editor", editor) + this.scope.add("view", view) + } + + + makeNewCommand() { + let plugin = this + function newCommand(f: any) { + plugin.addCommand({ + id: f.name, + name: f.name.replace("_", " "), + editorCallback: (editor: Editor, view: MarkdownView) => { + plugin.updateScopeApp() + plugin.updateScopeEditor(editor, view) + plugin.scope.run(f) + } + }); + return f + } + return newCommand + } runCommand(editor: Editor, view: MarkdownView, region: string) { let output: string = ""; - const endOfLine = makeEndOfLine(editor) + this.updateScopeApp() + this.updateScopeEditor(editor, view) - this.scope.add("saveExcursion", makeSaveExcursion(editor)) - - this.scope.add("lineNumber", makeLineNumber(editor)) - this.scope.add("command", makeCommand(this.app)) - this.scope.add("plugin", makePlugin(this.app)) - this.scope.add("bufferString", makeBufferString(editor)) - this.scope.add("insert", makeInsert(editor)) - this.scope.add("pointMin", pointMin) - this.scope.add("pointMax", makePointMax(editor)) - this.scope.add("forwardChar", makeForwardChar(editor)) - this.scope.add("message", message) - this.scope.add("repl", this) - this.scope.add("workspace", this.app.workspace) - this.scope.add("app", this.app) - this.scope.add("editor", editor) - this.scope.add("view", view) - this.scope.add("endOfLine", endOfLine) - this.scope.add("dir", dir) try { - output = formatObj(this.scope.eval(region)) + let result = this.scope.eval(region) + + const scope = this.scope + + if (result instanceof Promise) { + result.then((x) => { + scope.add("_", x) + }).catch((e) => { + scope.add("_error", e) + }) + } + + output = formatObj(result) } catch (e) { new Notice(e.toString()) throw e @@ -191,21 +327,61 @@ export default class ReplPlugin extends Plugin { async onload() { this.scope = new Scope() this.history = new History() + this.addCommands() + this.initLoaded = false + let plugin = this + this.app.workspace.on('active-leaf-change', () => { plugin.loadInit() }) + } + + makeSource(app: any) { + const plugin = this + async function source(name: string) { + plugin.updateScopeApp() + let path = name + ".md" + const file: string = await app.vault.getAbstractFileByPath(path) + const contents = await app.vault.read(file) + plugin.scope.eval(contents) + } + return source + } + + async loadInit() { + if (!this.initLoaded) { + let exists = await this.app.vault.exists("repl.md") + let source = this.makeSource(this.app) + if (exists) { + source("repl") + } + this.initLoaded = true + } + } + + //@ts-ignore + //if (await this.app.vault.exists("repl.md")) { + // await source("repl") + //} + + + addCommands() { this.addCommand({ id: 'repl-enter', name: 'Execute the current line or selection', editorCallback: (editor: Editor, view: MarkdownView) => { - const cursor = editor.getCursor() - const region = editor.getSelection() || editor.getLine(cursor.line) + try { + const cursor = editor.getCursor() + const region = editor.getSelection() || editor.getLine(cursor.line) - const endOfLine = makeEndOfLine(editor) + const endOfLine = makeEndOfLine(editor) - const output = this.runCommand(editor, view, region) + const output = this.runCommand(editor, view, region) - editor.setCursor(cursor.line + 1, 0) - editor.replaceRange(output + "\n", editor.getCursor()) - endOfLine() + editor.setCursor(cursor.line + 1, 0) + editor.replaceRange(output + "\n", editor.getCursor()) + endOfLine() + } catch (e) { + message(e.message) + } } });