How to edit MD source from a plugin?

Hi there!

I need some help with my first plugin, which is a fork from a plugin that adds a third state to the checkbox and gives it a new colour. However, I want to add more states to the checkbox. I am pretty sure I can figure out visual part of changing the checkbox (using decorations/view plugin (right??)). My struggle is however more to do with editing the MD source.

The thing I noticed when messing around with the code from the original plugin was that this plugin only edits the DOM and not the actual MD source. It uses a CSS style to show a different coloured checkbox as the “third” state and edits the dataset.task variable in the DOM to be “/” (forgive me if I am using the wrong terminology, I am not super familiar with JS and web tech). I would really like to directly edit the MD source of the checkboxes to signify different states of checkboxes so that these changes will sync nicely across my devices. How could I do this? After reading the docs I came across replaceRange but I am not sure how to access the EditorPosition inside the the checkbox. I would like to be able to edit it without having to move the cursor there. I found this method on the CodeMirror docs posAtDOM which I guess allows me to access the EditorPosition from a particular html node? But like I said I am not super familiar with web tech and could not figure out how to connect the codemirror method with the obsidian API.

Any help or info to send me in the right direction would be much appreciated!! If you have any questions, please let me know, and I will do my best to answer them.

Thanks!

Matt

(edit) Not sure why this has been flagged as spam. I am just trying to get some help for stuff that I couldn’t find in other forum posts nor in the obsidian docs. I have removed the links to github in case that is seen as “promotional”. It would be appreciated if people could teach me what is wrong with my post rather than flagging it as spam. Bunch of salty mfs. Help a brother out…

Assuming you can get a proper position info from posAtDOM, you can use this method to convert that position into EditorPosition.

But Obsidian API’s Editor interface is just a wrapper for CodeMirror, so you can also edit the document directly with CodeMirror. (But CodeMirror takes some time to learn)

1 Like

Cool, thanks for the tip!

I am trying to figure out what I need to import/call to use the code mirror Editor rather than the obsidian Editor API. I’ve tried to look at the view plugin docs but the example code will not start… It gives me a type error for the constructor function.

image

Should I create a separate topic to report this as a bug? Not sure if there is a problem on my end or if there is something wrong with the example. I’ve literally just copied the example code from View Plugins

I will keep plodding along and updating here if I find anything :slight_smile:

My guess is that you may be forgetting to register the view plugin.

// in the `onload` method of your Obsidian plugin
this.registerEditorExtension(examplePlugin);

One more important thing, you cannot edit the document via view plugins. View plugins only manipulate how the current state is displayed to users, and they cannot update the state itself.

For example, change the update method of the example view plugin to the following:

	update(update: ViewUpdate) {
		update.view.dispatch({
			changes: {from: 0, insert: "Hello world!"} // insert "Hello world!" at the beginning of the document
		})
	}

Then, you will see this error message:

Error: Calls to EditorView.update are not allowed while an update is in progress

If you want to make a command that modifies the document when it’s executed, maybe just something like this will suffice:

this.addCommand({
	id: "example-editor-command",
	name: "Example editor command",
	editorCallback: (editor: Editor, view: MarkdownView | MarkdownFileInfo) => {
		// @ts-ignore
		const editorView = editor.cm as EditorView;

        const tree = syntaxTree(editorView.state);

        // get the checkbox position (from & to) by parsing the syntax tree

		editorView.dispatch({
            changes: {from: ??, to: ??, insert: ??}
		});
	},
});

or

this.addCommand({
	id: "example-editor-command",
	name: "Example editor command",
	editorCallback: (editor: Editor, view: MarkdownView | MarkdownFileInfo) => {
		// @ts-ignore
		const editorView = editor.cm as EditorView;

        // get the checkbox position by parsing the DOM
        const dom = editorView.contentDOM.querySelector('<SOME CSS SELECTOR HERE>')
        if (!dom) return;
        const pos = editorView.posAtDOM(dom)

		editorView.dispatch({
            changes: {from: pos, to: pos + 1, insert: ??}
		});
	},
});

Right, that is useful to know.

Your suggestion indeed fixed my problem. Might be worth adding that to the view plugin docs as I could not see it anywhere.

I have managed to edit the document using replaceRange but now my issue is how do I extract either the offset or other position info from a DOM event? Because if I understand it correctly, I am interacting with the DOM when I click somewhere in the editor. So when I click on the checkbox, how to I find the position info? I had assumed posAtDOM would give me that info but I have yet to manage to get that to work :confused:

I am trying to edit the source MD as I click on a checkbox, rather than add a command.

Anyway, thanks for the great suggestions! These will definitely help me down the line :smiley:

In fact, it’s mentioned on another page of the editor extension section of the docs, as I linked in the previous reply. But I agree it’s better to include it in the view plugin page as well.

So when I click on the checkbox, how to I find the position info?

EditorView has another method called posAtCoords, which gives position info from the given screen coordinates.

When you click a dom, it triggers click event and this event object contains the screen coordinate of the clicked position. So you can

const pos = view.posAtCoords(event) ?? view.posAtCoords(event, false);

I am trying to edit the source MD as I click on a checkbox, rather than add a command.

OK, then register the editorCallback of the command as a DOM event handler to your plugin. I haven’t tested but something like this will work.

this.registerDomEvent(window, 'click', (event) => {
    // check if the clicked dom is a checkbox

    // @ts-ignore
    const editorView = this.app.workspace.activeEditor?.editor?.cm;
    if (!editorView) return;
    ...
})
1 Like

Amazing! How did I miss that?? You are a legend. Thank you for your help <3.

The forum software automatically flagged your posts because of all the links. It wasn’t a person. :robot:

Once you’re around a bit longer, this won’t happen. Sorry about that.

1 Like

Ahhh okay. My bad then. Thanks for restoring it!

Okay I feel like I have almost figured this out. Currently I am running this:

const view = this.app.workspace.getActiveViewOfType(MarkdownView);
const page_pos = view.editor.posAtCoords(evt.pageX, evt.pageY);
view.editor.replaceRange(
	"/",
	page_pos,
  );

The problem with this is that it never return the position within the brackets, only the position before or after the brackets.

Before the brackets:
image

After the brackets:
image

If I force the correct position using the offset, then I can edit between the brackets. However, it appends a space, so I add the end position as well:

const view = this.app.workspace.getActiveViewOfType(MarkdownView);
const pos = view.editor.offsetToPos(3);
const pos_end = view.editor.offsetToPos(4);
view.editor.replaceRange(
	"/",
	pos,
	pos_end
);

Result:
image

Any codemirror wizards out there who can tell me what is going wrong? I also still don’t understand why I can’t use view.editor.posAtDOM but I can use view.editor.posAtCoords. Both of them are mentioned in the codemirror docs (and not in the obsidian docs), but when I try to use posAtDOM it is not recognised as a function.

When @ush linked to the codemirror docs I kind of assumed that I would be able to use all the functions mentioned on that page (which includes posAtDOM), but that doesnt seem to be the case. What am I missing?

Hey view.editor isn’t a CodeMirror editor view itself, but view.editor.cm is (as mentioned here).

I really recommend you read through the docs first. Also, looking over my sample code above again will help.

As I said, Editor is a wrapper of the CodeMirror editor. So Editor and EditorView aren’t the same. When editor is an Editor instance, you can access the associated EditorView by editor.cm.

Obsidian uses CodeMirror (CM) as the underlying text editor, and exposes the CodeMirror editor as part of the API. Editor serves as an abstraction to bridge features between CM6 and CM5 (legacy editor, only available on desktop). By using Editor instead of directly accessing the CodeMirror instance, you ensure that your plugin works on both platforms.

From this page

1 Like

Thanks, I will read the docs again. You have been a great help, which I really appreciate :slight_smile:

1 Like

To manipulate the Markdown source in Obsidian plugins, you can use the editor API, particularly the editor.replaceRange method. Here’s a simple example of how you might use it:
const { Editor } = require(‘obsidian’);

class YourPlugin {
// … other plugin code

onInit() {
    // Access the editor
    this.editor = this.app.workspace.activeLeaf.view.sourceMode.cm;

    // Replace a range in the Markdown source
    this.replaceCheckboxState();
}

replaceCheckboxState() {
    // Find the line number and position where you want to make changes
    const lineNumber = 1; // replace with your desired line number
    const line = this.editor.getLine(lineNumber);
    const position = line.indexOf("[ ]");

    // Replace the checkbox state at the specified position
    this.editor.replaceRange("x", { line: lineNumber, ch: position }, { line: lineNumber, ch: position + 3 });
}

}

module.exports = YourPlugin;

1 Like