Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement and provide default mappings for some Obsidian-specific Vim motions/commands #222

Merged
merged 20 commits into from
Jul 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
ab1a283
feat: define and expose obsidian-specific vim commands
alythobani Apr 25, 2024
9e98ddf
Implement jumpToPreviousLink motion
alythobani Apr 27, 2024
0053c8f
Refactoring and implementing jumpToNextLink
alythobani Apr 27, 2024
128f183
refactor: new jumpToPattern function that can be used for motions
alythobani Apr 27, 2024
7ca0e78
refactor: renamed file and removed unneeded exports
alythobani Apr 27, 2024
5215915
fix: return last found index even if fewer than n instances found, in…
alythobani Apr 27, 2024
6779419
feat: implement moveUpSkipFold and moveDownSkipFold
alythobani Apr 27, 2024
be41f02
refactor: extract out helper functions for defining obsidian vim actions
alythobani Apr 27, 2024
45c4789
refactor: split vimApi.ts into two files
alythobani Apr 27, 2024
51fc17c
refactor: add comment
alythobani Apr 27, 2024
57bf405
refactor: update names, types, etc
alythobani Apr 27, 2024
5209149
feat: followLinkUnderCursor action
alythobani Apr 27, 2024
585c68c
feat: jumpToLink now jumps to both markdown and wiki links
alythobani Apr 27, 2024
3d457c2
refactor: rename fns
alythobani Apr 28, 2024
2adf45c
refactor: add docstrings / change var names
alythobani May 10, 2024
19d0958
feat: implement looping around
alythobani May 11, 2024
5953f5e
refactor: cleaner implementation of jumpToPattern
alythobani May 11, 2024
5b2a07d
Change mappings for next/prev heading to [[ and ]]
alythobani May 11, 2024
cd7d5e9
Tiny fixes
esm7 Jul 9, 2024
6789094
docs: update docs now that some more motions are provided by default
alythobani Jul 17, 2024
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: 3 additions & 4 deletions JsSnippets.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,11 @@ In this document I will collect some of my and user-contributed ideas for how to

If you have interesting snippets, please contribute by opening a pull request!

Note that these examples are included for demonstration purposes, and many of them are now provided by default in this plugin. Their actual implementations can be found under [`motions/`](https://github.com/esm7/obsidian-vimrc-support/blob/master/motions/), which you can also use as reference (either for your own custom motions, or if you wish to submit a PR for a new motion to be provided by this plugin).

## Jump to Next/Prev Markdown Header
## Jump to Next/Previous Markdown Heading

To map `]]` and `[[` to next/prev markdown header, I use the following.

In a file I call `mdHelpers.js`, put this:
In a file you can call `mdHelpers.js`, put this:

```js
// Taken from https://stackoverflow.com/questions/273789/is-there-a-version-of-javascripts-string-indexof-that-allows-for-regular-expr
Expand Down
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,13 @@ Commands that fail don't generate any visible error for now.
CodeMirror's Vim mode has some limitations and bugs and not all commands will work like you'd expect.
In some cases you can find workarounds by experimenting, and the easiest way to do that is by trying interactively rather than via the Vimrc file.

Finally, this plugin also provides the following motions/mappings by default:

- `[[` and `]]` to jump to the previous and next Markdown heading.
- `zk` and `zj` to move up and down while skipping folds.
- `gl` and `gL` to jump to the next and previous link.
- `gf` to open the link or file under the cursor (temporarily moving the cursor if necessary—e.g. if it's on the first square bracket of a [[Wikilink]]).

## Installation

In the Obsidian.md settings under "Community plugins", click on "Turn on community plugins", then browse to this plugin.
Expand Down Expand Up @@ -284,7 +291,7 @@ The `jsfile` should be placed in your vault (alongside, e.g., your markdown file

As above, the code running as part of `jsfile` has the arguments `editor: Editor`, `view: MarkdownView` and `selection: EditorSelection`.

Here's an example from my own `.obsidian.vimrc` that maps `]]` and `[[` to jump to the next/previous Markdown header:
Here's an example `.obsidian.vimrc` entry that maps `]]` and `[[` to jump to the next/previous Markdown heading. Note that `]]` and `[[` are already provided by default in this plugin, but this is a good example of how to use `jsfile`:

```
exmap nextHeading jsfile mdHelpers.js {jumpHeading(true)}
Expand Down
26 changes: 26 additions & 0 deletions actions/followLinkUnderCursor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { ObsidianActionFn } from "../utils/obsidianVimCommand";

/**
* Follows the link under the cursor, temporarily moving the cursor if necessary for follow-link to
* work (i.e. if the cursor is on a starting square bracket).
*/
export const followLinkUnderCursor: ObsidianActionFn = (vimrcPlugin) => {
const obsidianEditor = vimrcPlugin.getActiveObsidianEditor();
const { line, ch } = obsidianEditor.getCursor();
const firstTwoChars = obsidianEditor.getRange(
{ line, ch },
{ line, ch: ch + 2 }
);
let numCharsMoved = 0;
for (const char of firstTwoChars) {
if (char === "[") {
obsidianEditor.exec("goRight");
numCharsMoved++;
}
}
vimrcPlugin.executeObsidianCommand("editor:follow-link");
// Move the cursor back to where it was
for (let i = 0; i < numCharsMoved; i++) {
obsidianEditor.exec("goLeft");
}
};
43 changes: 43 additions & 0 deletions actions/moveSkippingFolds.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import VimrcPlugin from "../main";
import { ObsidianActionFn } from "../utils/obsidianVimCommand";

/**
* Moves the cursor down `repeat` lines, skipping over folded sections.
*/
export const moveDownSkippingFolds: ObsidianActionFn = (
vimrcPlugin,
cm,
{ repeat }
) => {
moveSkippingFolds(vimrcPlugin, repeat, "down");
};

/**
* Moves the cursor up `repeat` lines, skipping over folded sections.
*/
export const moveUpSkippingFolds: ObsidianActionFn = (
vimrcPlugin,
cm,
{ repeat }
) => {
moveSkippingFolds(vimrcPlugin, repeat, "up");
};

function moveSkippingFolds(
vimrcPlugin: VimrcPlugin,
repeat: number,
direction: "up" | "down"
) {
const obsidianEditor = vimrcPlugin.getActiveObsidianEditor();
let { line: oldLine, ch: oldCh } = obsidianEditor.getCursor();
const commandName = direction === "up" ? "goUp" : "goDown";
for (let i = 0; i < repeat; i++) {
obsidianEditor.exec(commandName);
const { line: newLine, ch: newCh } = obsidianEditor.getCursor();
if (newLine === oldLine && newCh === oldCh) {
// Going in the specified direction doesn't do anything anymore, stop now
return;
}
[oldLine, oldCh] = [newLine, newCh];
}
}
70 changes: 48 additions & 22 deletions main.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import * as keyFromAccelerator from 'keyboardevent-from-electron-accelerator';
import { EditorSelection, Notice, App, MarkdownView, Plugin, PluginSettingTab, Setting, TFile } from 'obsidian';
import { App, EditorSelection, MarkdownView, Notice, Editor as ObsidianEditor, Plugin, PluginSettingTab, Setting } from 'obsidian';

import { followLinkUnderCursor } from './actions/followLinkUnderCursor';
import { moveDownSkippingFolds, moveUpSkippingFolds } from './actions/moveSkippingFolds';
import { jumpToNextHeading, jumpToPreviousHeading } from './motions/jumpToHeading';
import { jumpToNextLink, jumpToPreviousLink } from './motions/jumpToLink';
import { defineAndMapObsidianVimAction, defineAndMapObsidianVimMotion } from './utils/obsidianVimCommand';
import { VimApi } from './utils/vimApi';

declare const CodeMirror: any;

Expand Down Expand Up @@ -247,6 +254,10 @@ export default class VimrcPlugin extends Plugin {
return this.app.workspace.getActiveViewOfType(MarkdownView);
}

getActiveObsidianEditor(): ObsidianEditor {
return this.getActiveView().editor;
}

private getCodeMirror(view: MarkdownView): CodeMirror.Editor {
return (view as any).editMode?.editor?.cm?.cm;
}
Expand All @@ -257,6 +268,7 @@ export default class VimrcPlugin extends Plugin {
var cmEditor = this.getCodeMirror(view);
if (cmEditor && !this.codeMirrorVimObject.loadedVimrc) {
this.defineBasicCommands(this.codeMirrorVimObject);
this.defineAndMapObsidianVimCommands(this.codeMirrorVimObject);
this.defineSendKeys(this.codeMirrorVimObject);
this.defineObCommand(this.codeMirrorVimObject);
this.defineSurround(this.codeMirrorVimObject);
Expand Down Expand Up @@ -367,6 +379,17 @@ export default class VimrcPlugin extends Plugin {
});
}

defineAndMapObsidianVimCommands(vimObject: VimApi) {
defineAndMapObsidianVimMotion(vimObject, jumpToNextHeading, ']]');
defineAndMapObsidianVimMotion(vimObject, jumpToPreviousHeading, '[[');
defineAndMapObsidianVimMotion(vimObject, jumpToNextLink, 'gl');
defineAndMapObsidianVimMotion(vimObject, jumpToPreviousLink, 'gL');

defineAndMapObsidianVimAction(vimObject, this, moveDownSkippingFolds, 'zj');
defineAndMapObsidianVimAction(vimObject, this, moveUpSkippingFolds, 'zk');
defineAndMapObsidianVimAction(vimObject, this, followLinkUnderCursor, 'gf');
}

defineSendKeys(vimObject: any) {
vimObject.defineEx('sendkeys', '', async (cm: any, params: any) => {
if (!params?.args?.length) {
Expand Down Expand Up @@ -401,33 +424,36 @@ export default class VimrcPlugin extends Plugin {
});
}

executeObsidianCommand(commandName: string) {
const availableCommands = (this.app as any).commands.commands;
if (!(commandName in availableCommands)) {
throw new Error(`Command ${commandName} was not found, try 'obcommand' with no params to see in the developer console what's available`);
}
const view = this.getActiveView();
const editor = view.editor;
const command = availableCommands[commandName];
const {callback, checkCallback, editorCallback, editorCheckCallback} = command;
if (editorCheckCallback)
editorCheckCallback(false, editor, view);
else if (editorCallback)
editorCallback(editor, view);
else if (checkCallback)
checkCallback(false);
else if (callback)
callback();
else
throw new Error(`Command ${commandName} doesn't have an Obsidian callback`);
}

defineObCommand(vimObject: any) {
vimObject.defineEx('obcommand', '', async (cm: any, params: any) => {
const availableCommands = (this.app as any).commands.commands;
if (!params?.args?.length || params.args.length != 1) {
const availableCommands = (this.app as any).commands.commands;
console.log(`Available commands: ${Object.keys(availableCommands).join('\n')}`)
throw new Error(`obcommand requires exactly 1 parameter`);
}
let view = this.getActiveView();
let editor = view.editor;
const command = params.args[0];
if (command in availableCommands) {
let callback = availableCommands[command].callback;
let checkCallback = availableCommands[command].checkCallback;
let editorCallback = availableCommands[command].editorCallback;
let editorCheckCallback = availableCommands[command].editorCheckCallback;
if (editorCheckCallback)
editorCheckCallback(false, editor, view);
else if (editorCallback)
editorCallback(editor, view);
else if (checkCallback)
checkCallback(false);
else if (callback)
callback();
else
throw new Error(`Command ${command} doesn't have an Obsidian callback`);
} else
throw new Error(`Command ${command} was not found, try 'obcommand' with no params to see in the developer console what's available`);
const commandName = params.args[0];
this.executeObsidianCommand(commandName);
});
}

Expand Down
34 changes: 34 additions & 0 deletions motions/jumpToHeading.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { jumpToPattern } from "../utils/jumpToPattern";
import { MotionFn } from "../utils/vimApi";

const HEADING_REGEX = /^#+ /gm;

/**
* Jumps to the repeat-th next heading.
*/
export const jumpToNextHeading: MotionFn = (cm, cursorPosition, { repeat }) => {
return jumpToPattern({
cm,
cursorPosition,
repeat,
regex: HEADING_REGEX,
direction: "next",
});
};

/**
* Jumps to the repeat-th previous heading.
*/
export const jumpToPreviousHeading: MotionFn = (
cm,
cursorPosition,
{ repeat }
) => {
return jumpToPattern({
cm,
cursorPosition,
repeat,
regex: HEADING_REGEX,
direction: "previous",
});
};
33 changes: 33 additions & 0 deletions motions/jumpToLink.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { jumpToPattern } from "../utils/jumpToPattern";
import { MotionFn } from "../utils/vimApi";

const WIKILINK_REGEX_STRING = "\\[\\[[^\\]\\]]+?\\]\\]";
const MARKDOWN_LINK_REGEX_STRING = "\\[[^\\]]+?\\]\\([^)]+?\\)";
const LINK_REGEX_STRING = `${WIKILINK_REGEX_STRING}|${MARKDOWN_LINK_REGEX_STRING}`;
const LINK_REGEX = new RegExp(LINK_REGEX_STRING, "g");

/**
* Jumps to the repeat-th next link.
*/
export const jumpToNextLink: MotionFn = (cm, cursorPosition, { repeat }) => {
return jumpToPattern({
cm,
cursorPosition,
repeat,
regex: LINK_REGEX,
direction: "next",
});
};

/**
* Jumps to the repeat-th previous link.
*/
export const jumpToPreviousLink: MotionFn = (cm, cursorPosition, { repeat }) => {
return jumpToPattern({
cm,
cursorPosition,
repeat,
regex: LINK_REGEX,
direction: "previous",
});
};
8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,15 @@
"@rollup/plugin-node-resolve": "^9.0.0",
"@rollup/plugin-typescript": "^11.0.0",
"@types/node": "^14.14.6",
"@types/string.prototype.matchall": "^4.0.4",
"codemirror": "^5.62.2",
"keyboardevent-from-electron-accelerator": "*",
"obsidian": "^1.1.1",
"rollup": "^2.33.0",
"tslib": "^2.0.3",
"typescript": "^4.9.4"
"tslib": "^2.6.3",
"typescript": "^5.5.3"
},
"dependencies": {
"string.prototype.matchall": "^4.0.11"
}
}
6 changes: 3 additions & 3 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@
"inlineSourceMap": true,
"inlineSources": true,
"module": "ESNext",
"target": "es5",
"target": "ES2020",
"allowJs": true,
"noImplicitAny": true,
"moduleResolution": "node",
"downlevelIteration": true,
"importHelpers": true,
"lib": [
"dom",
"es5",
"scripthost",
"es2015"
"ES2020"
]
},
"include": [
Expand Down
Loading