Accessing the vault from An Obsidian CodeMirror plugin

You can do lots of things throughout Obsidian through its native plugin API, however, Obsidian’s markdown editor uses a framework called CodeMirror, which means that if your plugin needs to iterate through visible markdown text and change how things are displayed, you will need to be working with the CodeMirror plugin API as well as Obsidian’s.

While working with the CodeMirror example code from the Obsidian docs, I was initially not sure how to access my Obsidian plugin instance and vault methods from within the CodeMirror code. So this article will describe how I approached it.

Obsidian example code

Below is the code from the Obsidian documentation on View Plugins. These are CodeMirror plugins that enable modifying how elements in a markdown file appear.

import {
  ViewUpdate,
  PluginValue,
  EditorView,
  ViewPlugin,
} from "@codemirror/view";

class ExampleViewPlugin implements PluginValue {
  constructor(view: EditorView) {
    // ...
  }

  update(update: ViewUpdate) {
    // ...
  }

  destroy() {
    // ...
  }
}

export const exampleViewPlugin = ViewPlugin.fromClass(ExamplePlugin);

To initialise this code from your plugin, in your main file, you would register the Editor extension.

import { Plugin } from "obsidian";
import { exampleViewPlugin } from "my-view-plugin";

export default class ExamplePlugin extends Plugin {
  async onload() {
    this.registerEditorExtension([exampleViewPlugin]); }
  }
  async onunload() {}
}

This gets your ViewPlugin up and going, and you can then start manipulating elements in the view and adorning them with decorations or replacing them with other elements.

The problem

The problem is, however, in this setup there is no way to get a reference to the plugin instance from within the ExampleViewPlugin class (and therefore the app or the vault instances either). We can’t pass it as a property into registerEditorExtension, and when the class is defined we don’t have a reference to it either.

While there’s a lot we can do without a reference to those, we can’t do things like access another file without them.

Solutions

To get around this, we need to wrap some of these processes in a function that we can pass the plugin into as a property. There are a couple of ways to do this that I considered.

Both methods involve placing your class definition inside the function so that the definition itself has access to the parameter you create to hold a reference to the plugin instance.

Option 1: Wrap everything in a function

This is my preferred solution, as it keeps the code in my main function simpler.

// Notice how this accepts a property for plugin
export function registerMyViewPlugin(plugin: MyPlugin) {

    // Notice how this is defined inside the function, so it can access the plugin property.
    class MyViewPlugin implements PluginValue {
      constructor(view: EditorView) {
        // Can access plugin from here now.
      }
      update(update: ViewUpdate) {
        // Can access plugin from here now.
      }
      destroy() {
        // Can access plugin from here now.
      }
    }

    // Notice that registration itself is inside here instead of inside main
    const myViewPlugin = ViewPlugin.fromClass(MyViewPlugin);
    plugin.registerEditorExtension(myViewPlugin);
}

This means my main function hides the implementation details complete so it’s more readable.

export default class ExamplePlugin extends Plugin {
  async onload() {
    registerMyViewPlugin();
  }
  // ...
}

Option 2: Wrap plugin creation in a function

This is a pattern I’ve seen in other plugins. It still hides a lot of implementation from the main function, but keeps a little there in order to facilitate batch registration of ViewPlugins.

Specifically, the registerEditorExtension line has moved from within the function to the main file and the naming of things has changed to reflect that.

export function getMyViewPlugin(plugin: MyPlugin) {

    class MyViewPlugin implements PluginValue {
      constructor(view: EditorView) {
          // ...
      }
      update(update: ViewUpdate) {
          // ...
      }
      destroy() {
          // ...
      }
    }

    return ViewPlugin.fromClass(MyViewPlugin);
}

Now that since registerEditorExtension accepts an array, we can now batch register other View Plugins at the same time.

export default class ExamplePlugin extends Plugin {
  async onload() {
      plugin.registerEditorExtension([
          getMyViewPlugin(this),
          getAnotherViewPlugin(this)
      ]);
  }
  // ...
}

Note
I prefer option 1 because it hides more from my main function, however, I’m unsure how well option 1 copes if you have multiple ViewPlugins to register as I haven’t tested it. Essentially the plugin would be calling registerEditorExtension multiple times instead of batching it.

While I haven’t tested it, I suspect it’s not an issue as registering occurs multiple times across different plugins anyway, however, not batching the registrations may slow down your plugin’s loading slightly if you have lots of ViewPlugins.

That’s it!

Before figuring this out, I asked on the Discord forum and the user @Koala suggested I wrap the ViewPlugin in a function, pointing to these examples: DataView View Definition, DataView Main. They’re slightly more complex, but have the same underlying approach.
Thanks @Koala!

Thanks…

I also dissect and speculate on design and development.
Digging into subtle details and implications, and exploring broad perspectives and potential paradigm shifts.
Check out my conceptual articles on Substack or find my latest below.


You can also find me on Threads, Bluesky, Mastodon, or X for more diverse posts about ongoing projects.

My latest articles

Focal point blocking for XR media

Planning out a linear VR experience requires thinking about where the viewers attention might be. Thinking about the focal points…

Designing immersive experiences

In traditional cinema, TV, or even the more modern phone screen, there’s limited screen real-estate. But removing that limitation creates a design problem…

The future is not prompt engineered

Let’s not pretend the importance of prompt engineering is ubiquitous. The most prevalent power of generative AI is in the way it adapts to us, not the other way around…

Author:

Date:

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.