mirror of
https://github.com/esm7/obsidian-vimrc-support.git
synced 2026-07-22 05:00:25 +00:00
Merge 0b1168b2ae into 1ff5d97afc
This commit is contained in:
commit
e9975172ce
9 changed files with 246 additions and 18 deletions
14
README.md
14
README.md
|
|
@ -86,11 +86,19 @@ In some cases you can find workarounds by experimenting, and the easiest way to
|
|||
|
||||
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.
|
||||
- `[[` and `]]` to jump to the previous and next Markdown heading. Supports repeat (e.g. `2]]` to jump two headings forward).
|
||||
- `zk` and `zj` to move up and down while skipping folds. Supports repeat.
|
||||
- `gc` and `gC` to jump to the next and previous Markdown code fence (opening or closing fence; just any line starting with >= 3 backticks). Supports repeat.
|
||||
- `gl` and `gL` to jump to the next and previous link. Supports repeat.
|
||||
- `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]]).
|
||||
|
||||
You can of course remap these as you wish. E.g. if you prefer `'h` and `gh` for jumping to headings:
|
||||
|
||||
```vim
|
||||
map 'h [[
|
||||
map gh ]]
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
In the Obsidian.md settings under "Community plugins", click on "Turn on community plugins", then browse to this plugin.
|
||||
|
|
|
|||
3
main.ts
3
main.ts
|
|
@ -3,6 +3,7 @@ import { App, EditorSelection, MarkdownView, Notice, Editor as ObsidianEditor, P
|
|||
|
||||
import { followLinkUnderCursor } from './actions/followLinkUnderCursor';
|
||||
import { moveDownSkippingFolds, moveUpSkippingFolds } from './actions/moveSkippingFolds';
|
||||
import { jumpToNextCodeFence, jumpToPreviousCodeFence } from './motions/jumpToCodeFence';
|
||||
import { jumpToNextHeading, jumpToPreviousHeading } from './motions/jumpToHeading';
|
||||
import { jumpToNextLink, jumpToPreviousLink } from './motions/jumpToLink';
|
||||
import { defineAndMapObsidianVimAction, defineAndMapObsidianVimMotion } from './utils/obsidianVimCommand';
|
||||
|
|
@ -429,6 +430,8 @@ export default class VimrcPlugin extends Plugin {
|
|||
defineAndMapObsidianVimCommands(vimObject: VimApi) {
|
||||
defineAndMapObsidianVimMotion(vimObject, jumpToNextHeading, ']]');
|
||||
defineAndMapObsidianVimMotion(vimObject, jumpToPreviousHeading, '[[');
|
||||
defineAndMapObsidianVimMotion(vimObject, jumpToNextCodeFence, 'gc');
|
||||
defineAndMapObsidianVimMotion(vimObject, jumpToPreviousCodeFence, 'gC');
|
||||
defineAndMapObsidianVimMotion(vimObject, jumpToNextLink, 'gl');
|
||||
defineAndMapObsidianVimMotion(vimObject, jumpToPreviousLink, 'gL');
|
||||
|
||||
|
|
|
|||
41
motions/jumpToCodeFence.ts
Normal file
41
motions/jumpToCodeFence.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import { jumpToPattern } from "../utils/jumpToPattern";
|
||||
import { MotionFn } from "../utils/vimApi";
|
||||
|
||||
/** Naive Regex for a Markdown code fence, i.e. a line beginning with at least three backticks.
|
||||
*
|
||||
* "Naive" for two reasons:
|
||||
* 1. False negatives: Fences could be made of tildes instead of backticks. This is less common, but
|
||||
* should be easy to add support for if users request it.
|
||||
* 2. False positives: It could match lines within non-Markdown codeblocks. But since triple
|
||||
* backticks aren't a syntax feature of any other programming language (AFAIK, at the time of this
|
||||
* writing), this should be rare enough that the naive regex should work well in practice.
|
||||
*
|
||||
* Matches only the fence itself, so can be used for jumping to either an opening or closing fence.
|
||||
*/
|
||||
const NAIVE_CODE_FENCE_REGEX = /^```+/gm;
|
||||
|
||||
/**
|
||||
* Jumps to the repeat-th next code fence.
|
||||
*/
|
||||
export const jumpToNextCodeFence: MotionFn = (cm, cursorPosition, { repeat }) => {
|
||||
return jumpToPattern({
|
||||
cm,
|
||||
cursorPosition,
|
||||
repeat,
|
||||
regex: NAIVE_CODE_FENCE_REGEX,
|
||||
direction: "next",
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Jumps to the repeat-th previous code fence.
|
||||
*/
|
||||
export const jumpToPreviousCodeFence: MotionFn = (cm, cursorPosition, { repeat }) => {
|
||||
return jumpToPattern({
|
||||
cm,
|
||||
cursorPosition,
|
||||
repeat,
|
||||
regex: NAIVE_CODE_FENCE_REGEX,
|
||||
direction: "previous",
|
||||
});
|
||||
};
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
import { Editor as CodeMirrorEditor } from "codemirror";
|
||||
import { EditorPosition } from "obsidian";
|
||||
import { isWithinMatch, jumpToPattern } from "../utils/jumpToPattern";
|
||||
import { MotionFn } from "../utils/vimApi";
|
||||
|
|
@ -11,6 +10,8 @@ const NAIVE_HEADING_REGEX = /^#{1,6} /gm;
|
|||
/** Regex for a Markdown fenced codeblock, which begins with some number >=3 of backticks at the
|
||||
* start of a line. It either ends on the nearest future line that starts with at least as many
|
||||
* backticks (\1 back-reference), or extends to the end of the string if no such future line exists.
|
||||
*
|
||||
* Matches the entire codeblock.
|
||||
*/
|
||||
const FENCED_CODEBLOCK_REGEX = /(^```+)(.*?^\1|.*)/gms;
|
||||
|
||||
|
|
@ -24,11 +25,7 @@ export const jumpToNextHeading: MotionFn = (cm, cursorPosition, { repeat }) => {
|
|||
/**
|
||||
* Jumps to the repeat-th previous heading.
|
||||
*/
|
||||
export const jumpToPreviousHeading: MotionFn = (
|
||||
cm,
|
||||
cursorPosition,
|
||||
{ repeat }
|
||||
) => {
|
||||
export const jumpToPreviousHeading: MotionFn = (cm, cursorPosition, { repeat }) => {
|
||||
return jumpToHeading({ cm, cursorPosition, repeat, direction: "previous" });
|
||||
};
|
||||
|
||||
|
|
@ -45,7 +42,7 @@ function jumpToHeading({
|
|||
repeat,
|
||||
direction,
|
||||
}: {
|
||||
cm: CodeMirrorEditor;
|
||||
cm: CodeMirror.Editor;
|
||||
cursorPosition: EditorPosition;
|
||||
repeat: number;
|
||||
direction: "next" | "previous";
|
||||
|
|
@ -62,7 +59,7 @@ function jumpToHeading({
|
|||
});
|
||||
}
|
||||
|
||||
function findAllCodeblocks(cm: CodeMirrorEditor): RegExpExecArray[] {
|
||||
function findAllCodeblocks(cm: CodeMirror.Editor): RegExpExecArray[] {
|
||||
const content = cm.getValue();
|
||||
return [...content.matchAll(FENCED_CODEBLOCK_REGEX)];
|
||||
}
|
||||
|
|
|
|||
38
tests/createFakeCodeMirrorEditor.ts
Normal file
38
tests/createFakeCodeMirrorEditor.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { EditorPosition } from "obsidian";
|
||||
|
||||
/**
|
||||
* Returns a fake CodeMirror editor bound to the given `contentLines`.
|
||||
*/
|
||||
export function createFakeCodeMirrorEditor(
|
||||
contentLines: string[]
|
||||
): Pick<CodeMirror.Editor, "getValue" | "indexFromPos" | "posFromIndex"> {
|
||||
const content = contentLines.join("\n");
|
||||
const lineStartIndexes = getLineStartIndexes(contentLines);
|
||||
return {
|
||||
getValue: () => content,
|
||||
indexFromPos: ({ line, ch }: EditorPosition) => lineStartIndexes[line] + ch,
|
||||
posFromIndex: (index: number) => getEditorPositionForIndex(index, lineStartIndexes),
|
||||
};
|
||||
}
|
||||
|
||||
function getLineStartIndexes(lines: string[]): number[] {
|
||||
const lineStartIndexes: number[] = [];
|
||||
let currentIndex = 0;
|
||||
for (const line of lines) {
|
||||
lineStartIndexes.push(currentIndex);
|
||||
currentIndex += line.length + 1;
|
||||
}
|
||||
return lineStartIndexes;
|
||||
}
|
||||
|
||||
function getEditorPositionForIndex(
|
||||
index: number,
|
||||
lineStartIndexes: number[]
|
||||
): EditorPosition {
|
||||
for (let line = lineStartIndexes.length - 1; line >= 0; line -= 1) {
|
||||
if (lineStartIndexes[line] <= index) {
|
||||
return { line, ch: index - lineStartIndexes[line] };
|
||||
}
|
||||
}
|
||||
return { line: 0, ch: 0 };
|
||||
}
|
||||
144
tests/jumpToCodeFence.test.ts
Normal file
144
tests/jumpToCodeFence.test.ts
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
import { EditorPosition } from "obsidian";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { jumpToNextCodeFence, jumpToPreviousCodeFence } from "../motions/jumpToCodeFence";
|
||||
import { createFakeCodeMirrorEditor } from "./createFakeCodeMirrorEditor";
|
||||
|
||||
const CODE_FENCE_CONTENT_LINES = ["intro", "```ts", "const answer = 42;", "```", "outro"];
|
||||
const INLINE_BACKTICK_CONTENT_LINES = [
|
||||
"here are inline backticks ``` not a fence",
|
||||
"```ts",
|
||||
"const answer = 42;",
|
||||
"```",
|
||||
];
|
||||
const MULTIPLE_CODE_FENCE_CONTENT_LINES = [
|
||||
"intro",
|
||||
"```ts",
|
||||
"const firstAnswer = 42;",
|
||||
"```",
|
||||
"middle",
|
||||
"````js",
|
||||
"const secondAnswer = 7;",
|
||||
"````",
|
||||
"outro",
|
||||
];
|
||||
const NO_CODE_FENCE_CONTENT_LINES = ["intro", "plain text", "outro"];
|
||||
|
||||
describe("jumpToNextCodeFence", () => {
|
||||
test("jumps to the next opening code fence", () => {
|
||||
expectNextCodeFencePosition({ line: 0, ch: 0 }, { line: 1, ch: 0 });
|
||||
});
|
||||
|
||||
test("jumps to the next closing code fence", () => {
|
||||
expectNextCodeFencePosition({ line: 2, ch: 0 }, { line: 3, ch: 0 });
|
||||
});
|
||||
|
||||
test("wraps to the first code fence after the last one", () => {
|
||||
expectNextCodeFencePosition({ line: 4, ch: 0 }, { line: 1, ch: 0 });
|
||||
});
|
||||
|
||||
test("ignores backticks that are not code fence lines", () => {
|
||||
expectNextCodeFencePosition(
|
||||
{ line: 0, ch: 0 },
|
||||
{ line: 1, ch: 0 },
|
||||
INLINE_BACKTICK_CONTENT_LINES
|
||||
);
|
||||
});
|
||||
|
||||
test("jumps to the second next code fence when repeat is two", () => {
|
||||
expectNextCodeFencePosition(
|
||||
{ line: 0, ch: 0 },
|
||||
{ line: 3, ch: 0 },
|
||||
MULTIPLE_CODE_FENCE_CONTENT_LINES,
|
||||
2
|
||||
);
|
||||
});
|
||||
|
||||
test("jumps to the closing code fence when cursor is within a code block", () => {
|
||||
expectNextCodeFencePosition({ line: 2, ch: 5 }, { line: 3, ch: 0 });
|
||||
});
|
||||
|
||||
test("jumps to the next distinct code fence when cursor is on the first backtick of a fence", () => {
|
||||
expectNextCodeFencePosition({ line: 1, ch: 0 }, { line: 3, ch: 0 });
|
||||
});
|
||||
|
||||
test("jumps to the next distinct code fence when cursor is in the middle of a fence", () => {
|
||||
expectNextCodeFencePosition({ line: 1, ch: 1 }, { line: 3, ch: 0 });
|
||||
});
|
||||
|
||||
test("returns the original cursor position when there are no code fences", () => {
|
||||
expectNextCodeFencePosition(
|
||||
{ line: 1, ch: 2 },
|
||||
{ line: 1, ch: 2 },
|
||||
NO_CODE_FENCE_CONTENT_LINES
|
||||
);
|
||||
});
|
||||
|
||||
test("matches fences longer than three backticks", () => {
|
||||
expectNextCodeFencePosition(
|
||||
{ line: 4, ch: 0 },
|
||||
{ line: 5, ch: 0 },
|
||||
MULTIPLE_CODE_FENCE_CONTENT_LINES
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("jumpToPreviousCodeFence", () => {
|
||||
test("jumps to the previous closing code fence", () => {
|
||||
expectPreviousCodeFencePosition({ line: 4, ch: 0 }, { line: 3, ch: 0 });
|
||||
});
|
||||
|
||||
test("jumps to the previous opening code fence", () => {
|
||||
expectPreviousCodeFencePosition({ line: 2, ch: 0 }, { line: 1, ch: 0 });
|
||||
});
|
||||
|
||||
test("jumps to the second previous code fence when repeat is two", () => {
|
||||
expectPreviousCodeFencePosition(
|
||||
{ line: 8, ch: 0 },
|
||||
{ line: 5, ch: 0 },
|
||||
MULTIPLE_CODE_FENCE_CONTENT_LINES,
|
||||
2
|
||||
);
|
||||
});
|
||||
|
||||
test("jumps to the opening code fence when cursor is within a code block", () => {
|
||||
expectPreviousCodeFencePosition({ line: 2, ch: 5 }, { line: 1, ch: 0 });
|
||||
});
|
||||
|
||||
test("jumps to the previous distinct code fence when cursor is on the first backtick of a fence", () => {
|
||||
expectPreviousCodeFencePosition({ line: 3, ch: 0 }, { line: 1, ch: 0 });
|
||||
});
|
||||
|
||||
test("jumps to the previous distinct code fence when cursor is in the middle of a fence", () => {
|
||||
expectPreviousCodeFencePosition({ line: 3, ch: 1 }, { line: 1, ch: 0 });
|
||||
});
|
||||
|
||||
test("returns the original cursor position when there are no code fences", () => {
|
||||
expectPreviousCodeFencePosition(
|
||||
{ line: 1, ch: 2 },
|
||||
{ line: 1, ch: 2 },
|
||||
NO_CODE_FENCE_CONTENT_LINES
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
function expectNextCodeFencePosition(
|
||||
cursorPosition: EditorPosition,
|
||||
expectedPosition: EditorPosition,
|
||||
contentLines: string[] = CODE_FENCE_CONTENT_LINES,
|
||||
repeat = 1
|
||||
): void {
|
||||
const cm = createFakeCodeMirrorEditor(contentLines);
|
||||
const nextCodeFence = jumpToNextCodeFence(cm as any, cursorPosition, { repeat });
|
||||
expect(nextCodeFence).toEqual(expectedPosition);
|
||||
}
|
||||
|
||||
function expectPreviousCodeFencePosition(
|
||||
cursorPosition: EditorPosition,
|
||||
expectedPosition: EditorPosition,
|
||||
contentLines: string[] = CODE_FENCE_CONTENT_LINES,
|
||||
repeat = 1
|
||||
): void {
|
||||
const cm = createFakeCodeMirrorEditor(contentLines);
|
||||
const previousCodeFence = jumpToPreviousCodeFence(cm as any, cursorPosition, { repeat });
|
||||
expect(previousCodeFence).toEqual(expectedPosition);
|
||||
}
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
import { Editor as CodeMirrorEditor } from "codemirror";
|
||||
import { EditorPosition } from "obsidian";
|
||||
|
||||
/**
|
||||
|
|
@ -27,7 +26,7 @@ export function jumpToPattern({
|
|||
filterMatch = () => true,
|
||||
direction,
|
||||
}: {
|
||||
cm: CodeMirrorEditor;
|
||||
cm: CodeMirror.Editor;
|
||||
cursorPosition: EditorPosition;
|
||||
repeat: number;
|
||||
regex: RegExp;
|
||||
|
|
|
|||
|
|
@ -2,14 +2,13 @@
|
|||
* Utility types and functions for defining Obsidian-specific Vim commands.
|
||||
*/
|
||||
|
||||
import { Editor as CodeMirrorEditor } from "codemirror";
|
||||
|
||||
import VimrcPlugin from "../main";
|
||||
import { MotionFn, VimApi } from "./vimApi";
|
||||
|
||||
export type ObsidianActionFn = (
|
||||
vimrcPlugin: VimrcPlugin, // Included so we can run Obsidian commands as part of the action
|
||||
cm: CodeMirrorEditor,
|
||||
cm: CodeMirror.Editor,
|
||||
actionArgs: { repeat: number },
|
||||
) => void;
|
||||
|
||||
|
|
|
|||
|
|
@ -6,17 +6,16 @@
|
|||
* https://libvoyant.ucr.edu/resources/codemirror/doc/manual.html
|
||||
*/
|
||||
|
||||
import { Editor as CodeMirrorEditor } from "codemirror";
|
||||
import { EditorPosition } from "obsidian";
|
||||
|
||||
export type MotionFn = (
|
||||
cm: CodeMirrorEditor,
|
||||
cm: CodeMirror.Editor,
|
||||
cursorPosition: EditorPosition, // called `head` in the API
|
||||
motionArgs: { repeat: number }
|
||||
) => EditorPosition;
|
||||
|
||||
export type ActionFn = (
|
||||
cm: CodeMirrorEditor,
|
||||
cm: CodeMirror.Editor,
|
||||
actionArgs: { repeat: number },
|
||||
) => void;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue