-
Notifications
You must be signed in to change notification settings - Fork 68
feat: provide new motions for jumping to the previous or next Markdown code fence #284
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
base: master
Are you sure you want to change the base?
Changes from all commits
827dea5
fdc3aa0
b46f370
803ad68
97e75ae
c15140e
bcad4fd
72e9320
80721a3
0b1168b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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", | ||
| }); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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), | ||
| }; | ||
| } | ||
|
Comment on lines
+3
to
+16
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added this |
||
|
|
||
| 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 }; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Realized we can just use the
CodeMirror.Editortype without needing to import it, so went ahead and removed these unnecessary aliases I'd added in a previous PR