-
Notifications
You must be signed in to change notification settings - Fork 60
Implement and provide default mappings for some Obsidian-specific Vim motions/commands #222
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
Merged
Merged
Changes from 18 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 9e98ddf
Implement jumpToPreviousLink motion
alythobani 0053c8f
Refactoring and implementing jumpToNextLink
alythobani 128f183
refactor: new jumpToPattern function that can be used for motions
alythobani 7ca0e78
refactor: renamed file and removed unneeded exports
alythobani 5215915
fix: return last found index even if fewer than n instances found, in…
alythobani 6779419
feat: implement moveUpSkipFold and moveDownSkipFold
alythobani be41f02
refactor: extract out helper functions for defining obsidian vim actions
alythobani 45c4789
refactor: split vimApi.ts into two files
alythobani 51fc17c
refactor: add comment
alythobani 57bf405
refactor: update names, types, etc
alythobani 5209149
feat: followLinkUnderCursor action
alythobani 585c68c
feat: jumpToLink now jumps to both markdown and wiki links
alythobani 3d457c2
refactor: rename fns
alythobani 2adf45c
refactor: add docstrings / change var names
alythobani 19d0958
feat: implement looping around
alythobani 5953f5e
refactor: cleaner implementation of jumpToPattern
alythobani 5b2a07d
Change mappings for next/prev heading to [[ and ]]
alythobani cd7d5e9
Tiny fixes
esm7 6789094
docs: update docs now that some more motions are provided by default
alythobani File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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"); | ||
| } | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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]; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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", | ||
| }); | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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", | ||
| }); | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| import { Editor as CodeMirrorEditor } from "codemirror"; | ||
| import { EditorPosition } from "obsidian"; | ||
| import { shim as matchAllShim } from "string.prototype.matchall"; | ||
|
|
||
| // Polyfill for String.prototype.matchAll, in case it's not available (pre-ES2020) | ||
| matchAllShim(); | ||
|
|
||
| /** | ||
| * Returns the position of the repeat-th instance of a pattern from a given cursor position, in the | ||
| * given direction; looping to the other end of the document when reaching one end. Returns the | ||
| * original cursor position if no match is found. | ||
| * | ||
| * Under the hood, to avoid repeated loops of the document: we get all matches at once, order them | ||
| * according to `direction` and `cursorPosition`, and use modulo arithmetic to return the | ||
| * appropriate match. | ||
| */ | ||
| export function jumpToPattern({ | ||
| cm, | ||
| cursorPosition, | ||
| repeat, | ||
| regex, | ||
| direction, | ||
| }: { | ||
| cm: CodeMirrorEditor; | ||
| cursorPosition: EditorPosition; | ||
| repeat: number; | ||
| regex: RegExp; | ||
| direction: "next" | "previous"; | ||
| }): EditorPosition { | ||
| const content = cm.getValue(); | ||
| const cursorIdx = cm.indexFromPos(cursorPosition); | ||
| const orderedMatches = getOrderedMatches({ | ||
| content, | ||
| regex, | ||
| cursorIdx, | ||
| direction, | ||
| }); | ||
| const effectiveRepeat = (repeat % orderedMatches.length) || orderedMatches.length; | ||
| const matchIdx = orderedMatches[effectiveRepeat - 1]?.index; | ||
| if (matchIdx === undefined) { | ||
| return cursorPosition; | ||
| } | ||
| const newCursorPosition = cm.posFromIndex(matchIdx); | ||
| return newCursorPosition; | ||
| } | ||
|
|
||
| /** | ||
| * Returns an ordered array of all matches of a regex in a string in the given direction from the | ||
| * cursor index (looping around to the other end of the document when reaching one end). | ||
| */ | ||
| function getOrderedMatches({ | ||
| content, | ||
| regex, | ||
| cursorIdx, | ||
| direction, | ||
| }: { | ||
| content: string; | ||
| regex: RegExp; | ||
| cursorIdx: number; | ||
| direction: "next" | "previous"; | ||
| }): RegExpExecArray[] { | ||
| const { previousMatches, currentMatches, nextMatches } = getAndGroupMatches( | ||
| content, | ||
| regex, | ||
| cursorIdx | ||
| ); | ||
| if (direction === "next") { | ||
| return [...nextMatches, ...previousMatches, ...currentMatches]; | ||
| } | ||
| return [ | ||
| ...previousMatches.reverse(), | ||
| ...nextMatches.reverse(), | ||
| ...currentMatches.reverse(), | ||
| ]; | ||
| } | ||
|
|
||
| /** | ||
| * Finds all matches of a regex in a string and groups them by their positions relative to the | ||
| * cursor. | ||
| */ | ||
| function getAndGroupMatches( | ||
| content: string, | ||
| regex: RegExp, | ||
| cursorIdx: number | ||
| ): { | ||
| previousMatches: RegExpExecArray[]; | ||
| currentMatches: RegExpExecArray[]; | ||
| nextMatches: RegExpExecArray[]; | ||
| } { | ||
| const globalRegex = makeGlobalRegex(regex); | ||
| const allMatches = [...content.matchAll(globalRegex)]; | ||
| const previousMatches = allMatches.filter( | ||
| (match) => match.index < cursorIdx && !isCursorOnMatch(match, cursorIdx) | ||
| ); | ||
| const currentMatches = allMatches.filter((match) => | ||
| isCursorOnMatch(match, cursorIdx) | ||
| ); | ||
| const nextMatches = allMatches.filter((match) => match.index > cursorIdx); | ||
| return { previousMatches, currentMatches, nextMatches }; | ||
| } | ||
|
|
||
| function makeGlobalRegex(regex: RegExp): RegExp { | ||
| const globalFlags = getGlobalFlags(regex); | ||
| return new RegExp(regex.source, globalFlags); | ||
| } | ||
|
|
||
| function getGlobalFlags(regex: RegExp): string { | ||
| const { flags } = regex; | ||
| return flags.includes("g") ? flags : `${flags}g`; | ||
| } | ||
|
|
||
| function isCursorOnMatch(match: RegExpExecArray, cursorIdx: number): boolean { | ||
| return match.index <= cursorIdx && cursorIdx < match.index + match[0].length; | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
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.
I'm guessing we don't actually need this polyfill since users are probably using a version of Obsidian that runs on a post-2020 Electron/Chromium version (which will have
String.matchAllimplemented). In which case we should updatetsconfigto specifyES2020fortargetandlib