├── global.d.ts ├── .gitignore ├── icon.png ├── screencast.gif ├── postcss.config.js ├── tailwind.config.js ├── src ├── commands │ ├── help.ts │ ├── index.ts │ ├── invoke.ts │ ├── mark.ts │ ├── emoji.ts │ ├── lorem.ts │ ├── bg.ts │ ├── sort.ts │ ├── go.ts │ └── page.ts ├── style.css ├── App.vue ├── vue.d.ts ├── common │ └── type.ts ├── stores │ ├── help.ts │ ├── copy-text.ts │ ├── mark.ts │ ├── emoji.ts │ ├── command.ts │ └── color.ts ├── components │ ├── Search.vue │ ├── Color.vue │ ├── CopyText.vue │ ├── Mark.vue │ ├── Help.vue │ └── Command.vue └── keybindings │ ├── exitEditing.ts │ ├── outdent.ts │ ├── indent.ts │ ├── left.ts │ ├── lineEnd.ts │ ├── right.ts │ ├── wordEnd.ts │ ├── findChar.ts │ ├── wordForward.ts │ ├── wordBackward.ts │ ├── extend.ts │ ├── collapse.ts │ ├── findCharBackward.ts │ ├── repeatCharSearch.ts │ ├── repeatCharSearchReverse.ts │ ├── redo.ts │ ├── undo.ts │ ├── toggleVisualMode.ts │ ├── number.ts │ ├── changeCaseLowerCase.ts │ ├── changeCaseUpperCase.ts │ ├── top.ts │ ├── searchBaidu.ts │ ├── searchGithub.ts │ ├── bottom.ts │ ├── searchGoogle.ts │ ├── searchWikipedia.ts │ ├── searchYoutube.ts │ ├── emoji.ts │ ├── searchStackoverflow.ts │ ├── pastePrev.ts │ ├── pasteNext.ts │ ├── copyCurrentBlockContent.ts │ ├── prevNewBlock.ts │ ├── cut.ts │ ├── nextNewBlock.ts │ ├── cutWord.ts │ ├── copyCurrentBlockRef.ts │ ├── highlightFocusOut.ts │ ├── extendAll.ts │ ├── collapseAll.ts │ ├── decrease.ts │ ├── increase.ts │ ├── highlightFocusIn.ts │ ├── up.ts │ ├── down.ts │ ├── backgroundColor.ts │ ├── joinNextLine.ts │ ├── insertBefore.ts │ ├── jumpInto.ts │ ├── command.ts │ ├── insert.ts │ ├── sort.ts │ ├── prevSibling.ts │ ├── nextSibling.ts │ ├── search.ts │ ├── changeCurrentBlock.ts │ ├── deleteCurrentBlock.ts │ ├── deleteCurrentAndNextSiblingBlocks.ts │ ├── deleteCurrentAndPrevSiblingBlocks.ts │ ├── mark.ts │ └── changeCase.ts ├── .editorconfig ├── index.html ├── LICENSE ├── vite.config.ts ├── components.d.ts ├── package.json ├── AGENTS.md ├── .github └── workflows │ └── publish.yml ├── auto-imports.d.ts ├── CHANGELOG.md ├── tsconfig.json └── README.md /global.d.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | .DS_Store 4 | .vscode/ 5 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vipzhicheng/logseq-plugin-vim-shortcuts/HEAD/icon.png -------------------------------------------------------------------------------- /screencast.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vipzhicheng/logseq-plugin-vim-shortcuts/HEAD/screencast.gif -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ["index.html", "src/**/*.vue"], 3 | theme: { 4 | extend: {}, 5 | }, 6 | plugins: [], 7 | }; 8 | -------------------------------------------------------------------------------- /src/commands/help.ts: -------------------------------------------------------------------------------- 1 | import { useHelpStore } from "@/stores/help"; 2 | export function show() { 3 | const helpStore = useHelpStore(); 4 | helpStore.show(); 5 | } 6 | -------------------------------------------------------------------------------- /src/style.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | mark.vim-shortcuts-highlight { 6 | padding-left: 0 !important; 7 | padding-right: 0 !important; 8 | } 9 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/vue.d.ts: -------------------------------------------------------------------------------- 1 | // 2 | 3 | declare module "*.vue" { 4 | import { DefineComponent } from "vue"; 5 | const component: DefineComponent<{}, {}, any>; 6 | export default component; 7 | } 8 | -------------------------------------------------------------------------------- /src/common/type.ts: -------------------------------------------------------------------------------- 1 | 2 | export type TempCache = { 3 | clipboard: string 4 | lastPage: string 5 | visualMode: boolean 6 | }; 7 | 8 | export type N = { 9 | n: number 10 | lastChange: Date | null 11 | }; 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.md] 11 | max_line_length = off 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /src/stores/help.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from "pinia"; 2 | export const useHelpStore = defineStore("help", { 3 | state: () => ({ 4 | visible: false, 5 | }), 6 | actions: { 7 | toggle() { 8 | this.visible = !this.visible; 9 | }, 10 | 11 | show() { 12 | this.visible = true; 13 | }, 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /src/commands/index.ts: -------------------------------------------------------------------------------- 1 | export * as go from "./go"; 2 | export * as help from "./help"; 3 | export * as mark from "./mark"; 4 | export * as page from "./page"; 5 | export * as invoke from "./invoke"; 6 | export * as lorem from "./lorem"; 7 | export * as sort from "./sort"; 8 | export * as emoji from "./emoji"; 9 | export * as bg from "./bg"; 10 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vim shortcuts 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/commands/invoke.ts: -------------------------------------------------------------------------------- 1 | import "@logseq/libs"; 2 | 3 | export async function redo() { 4 | // @ts-ignore 5 | await logseq.App.invokeExternalCommand("logseq.editor/redo"); 6 | } 7 | 8 | export async function undo() { 9 | // @ts-ignore 10 | await logseq.App.invokeExternalCommand("logseq.editor/undo"); 11 | } 12 | 13 | export async function backward() { 14 | // @ts-ignore 15 | await logseq.App.invokeExternalCommand("logseq.go/backward"); 16 | } 17 | 18 | export async function forward() { 19 | // @ts-ignore 20 | await logseq.App.invokeExternalCommand("logseq.go/forward"); 21 | } 22 | -------------------------------------------------------------------------------- /src/stores/copy-text.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from "pinia"; 2 | import "@logseq/libs"; 3 | 4 | export const useCopyTextStore = defineStore("copy-text", { 5 | state: () => ({ 6 | visible: false, 7 | title: "Copy Text", 8 | width: "50%", 9 | content: "", 10 | }), 11 | actions: { 12 | show() { 13 | this.visible = true; 14 | }, 15 | hide() { 16 | this.visible = false; 17 | }, 18 | setTitle(title: string) { 19 | this.title = title; 20 | }, 21 | setWidth(width: string) { 22 | this.width = width; 23 | }, 24 | setContent(content: string) { 25 | this.content = content; 26 | }, 27 | }, 28 | }); 29 | -------------------------------------------------------------------------------- /src/components/Search.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 24 | 25 | 30 | -------------------------------------------------------------------------------- /src/components/Color.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/keybindings/exitEditing.ts: -------------------------------------------------------------------------------- 1 | import { ILSPluginUser } from "@logseq/libs/dist/LSPlugin"; 2 | import { debug, getSettings } from "@/common/funcs"; 3 | 4 | export default (logseq: ILSPluginUser) => { 5 | const settings = getSettings(); 6 | 7 | const bindings = Array.isArray(settings.keyBindings.exitEditing) 8 | ? settings.keyBindings.exitEditing 9 | : [settings.keyBindings.exitEditing]; 10 | 11 | bindings.forEach((binding, index) => { 12 | logseq.App.registerCommandPalette( 13 | { 14 | key: "vim-shortcut-exit-editing-" + index, 15 | label: "Exit editing", 16 | keybinding: { 17 | mode: "global", 18 | binding, 19 | }, 20 | }, 21 | async () => { 22 | debug("Exit editing"); 23 | await logseq.Editor.exitEditingMode(true); 24 | } 25 | ); 26 | }); 27 | }; 28 | -------------------------------------------------------------------------------- /src/keybindings/outdent.ts: -------------------------------------------------------------------------------- 1 | import { ILSPluginUser } from "@logseq/libs/dist/LSPlugin"; 2 | import { debug, getSettings } from "@/common/funcs"; 3 | 4 | export default (logseq: ILSPluginUser) => { 5 | const settings = getSettings(); 6 | 7 | const bindings = Array.isArray(settings.keyBindings.outdent) 8 | ? settings.keyBindings.outdent 9 | : [settings.keyBindings.outdent]; 10 | 11 | bindings.forEach((binding, index) => { 12 | logseq.App.registerCommandPalette( 13 | { 14 | key: "vim-shortcut-outdent-" + index, 15 | label: "outdent", 16 | keybinding: { 17 | mode: "non-editing", 18 | binding, 19 | }, 20 | }, 21 | async () => { 22 | debug("Outdent"); 23 | 24 | // @ts-ignore 25 | await logseq.App.invokeExternalCommand("logseq.editor/outdent"); 26 | } 27 | ); 28 | }); 29 | }; 30 | -------------------------------------------------------------------------------- /src/commands/mark.ts: -------------------------------------------------------------------------------- 1 | import "@logseq/libs"; 2 | import { useMarkStore } from "@/stores/mark"; 3 | 4 | export function marks() { 5 | const markStore = useMarkStore(); 6 | markStore.reload(); 7 | markStore.toggle(); 8 | } 9 | 10 | export async function deleteMarks(ids: string[]) { 11 | const markStore = useMarkStore(); 12 | await markStore.deleteMarks(ids); 13 | markStore.reload(); 14 | } 15 | 16 | export function mark(id: string) { 17 | const markStore = useMarkStore(); 18 | const m = markStore.getMark(id); 19 | if (m) { 20 | if (m.block) { 21 | logseq.Editor.scrollToBlockInPage(m.page, m.block); 22 | } else { 23 | logseq.App.pushState("page", { 24 | name: m.page, 25 | }); 26 | } 27 | } 28 | } 29 | 30 | export async function clearMarks() { 31 | const markStore = useMarkStore(); 32 | await markStore.clearMarks(); 33 | markStore.reload(); 34 | } 35 | -------------------------------------------------------------------------------- /src/keybindings/indent.ts: -------------------------------------------------------------------------------- 1 | import { ILSPluginUser } from "@logseq/libs/dist/LSPlugin"; 2 | import { debug, getNumber, getSettings, resetNumber } from "@/common/funcs"; 3 | 4 | export default (logseq: ILSPluginUser) => { 5 | const settings = getSettings(); 6 | 7 | const bindings = Array.isArray(settings.keyBindings.indent) 8 | ? settings.keyBindings.indent 9 | : [settings.keyBindings.indent]; 10 | 11 | bindings.forEach((binding, index) => { 12 | logseq.App.registerCommandPalette( 13 | { 14 | key: "vim-shortcut-indent-" + index, 15 | label: "indent", 16 | keybinding: { 17 | mode: "non-editing", 18 | binding, 19 | }, 20 | }, 21 | async () => { 22 | debug("Indent"); 23 | 24 | // @ts-ignore 25 | await logseq.App.invokeExternalCommand("logseq.editor/indent"); 26 | } 27 | ); 28 | }); 29 | }; 30 | -------------------------------------------------------------------------------- /src/keybindings/left.ts: -------------------------------------------------------------------------------- 1 | import { ILSPluginUser } from "@logseq/libs/dist/LSPlugin"; 2 | import { debug, getSettings } from "@/common/funcs"; 3 | import { useSearchStore } from "@/stores/search"; 4 | 5 | export default (logseq: ILSPluginUser) => { 6 | const settings = getSettings(); 7 | 8 | const bindings = Array.isArray(settings.keyBindings.left) 9 | ? settings.keyBindings.left 10 | : [settings.keyBindings.left]; 11 | 12 | bindings.forEach((binding, index) => { 13 | logseq.App.registerCommandPalette( 14 | { 15 | key: "vim-shortcut-left-" + index, 16 | label: "Move cursor left", 17 | keybinding: { 18 | mode: "non-editing", 19 | binding, 20 | }, 21 | }, 22 | async () => { 23 | debug("Move cursor left"); 24 | const searchStore = useSearchStore(); 25 | await searchStore.moveCursorLeft(); 26 | } 27 | ); 28 | }); 29 | }; 30 | -------------------------------------------------------------------------------- /src/keybindings/lineEnd.ts: -------------------------------------------------------------------------------- 1 | import { ILSPluginUser } from "@logseq/libs/dist/LSPlugin"; 2 | import { debug, getSettings } from "@/common/funcs"; 3 | import { useSearchStore } from "@/stores/search"; 4 | 5 | export default (logseq: ILSPluginUser) => { 6 | const settings = getSettings(); 7 | 8 | const bindings = Array.isArray(settings.keyBindings.lineEnd) 9 | ? settings.keyBindings.lineEnd 10 | : [settings.keyBindings.lineEnd]; 11 | 12 | bindings.forEach((binding, index) => { 13 | logseq.App.registerCommandPalette( 14 | { 15 | key: "vim-shortcut-line-end-" + index, 16 | label: "Move to line end ($)", 17 | keybinding: { 18 | mode: "non-editing", 19 | binding, 20 | }, 21 | }, 22 | async () => { 23 | debug("Line end"); 24 | const searchStore = useSearchStore(); 25 | await searchStore.moveLineEnd(); 26 | } 27 | ); 28 | }); 29 | }; 30 | -------------------------------------------------------------------------------- /src/keybindings/right.ts: -------------------------------------------------------------------------------- 1 | import { ILSPluginUser } from "@logseq/libs/dist/LSPlugin"; 2 | import { debug, getSettings } from "@/common/funcs"; 3 | import { useSearchStore } from "@/stores/search"; 4 | 5 | export default (logseq: ILSPluginUser) => { 6 | const settings = getSettings(); 7 | 8 | const bindings = Array.isArray(settings.keyBindings.right) 9 | ? settings.keyBindings.right 10 | : [settings.keyBindings.right]; 11 | 12 | bindings.forEach((binding, index) => { 13 | logseq.App.registerCommandPalette( 14 | { 15 | key: "vim-shortcut-right-" + index, 16 | label: "Move cursor right", 17 | keybinding: { 18 | mode: "non-editing", 19 | binding, 20 | }, 21 | }, 22 | async () => { 23 | debug("Move cursor right"); 24 | const searchStore = useSearchStore(); 25 | await searchStore.moveCursorRight(); 26 | } 27 | ); 28 | }); 29 | }; 30 | -------------------------------------------------------------------------------- /src/keybindings/wordEnd.ts: -------------------------------------------------------------------------------- 1 | import { ILSPluginUser } from "@logseq/libs/dist/LSPlugin"; 2 | import { debug, getSettings } from "@/common/funcs"; 3 | import { useSearchStore } from "@/stores/search"; 4 | 5 | export default (logseq: ILSPluginUser) => { 6 | const settings = getSettings(); 7 | 8 | const bindings = Array.isArray(settings.keyBindings.wordEnd) 9 | ? settings.keyBindings.wordEnd 10 | : [settings.keyBindings.wordEnd]; 11 | 12 | bindings.forEach((binding, index) => { 13 | logseq.App.registerCommandPalette( 14 | { 15 | key: "vim-shortcut-word-end-" + index, 16 | label: "Move to word end (e)", 17 | keybinding: { 18 | mode: "non-editing", 19 | binding, 20 | }, 21 | }, 22 | async () => { 23 | debug("Word end"); 24 | const searchStore = useSearchStore(); 25 | await searchStore.moveWordEnd(); 26 | } 27 | ); 28 | }); 29 | }; 30 | -------------------------------------------------------------------------------- /src/keybindings/findChar.ts: -------------------------------------------------------------------------------- 1 | import { ILSPluginUser } from "@logseq/libs/dist/LSPlugin"; 2 | import { debug, getSettings } from "@/common/funcs"; 3 | import { useSearchStore } from "@/stores/search"; 4 | 5 | export default (logseq: ILSPluginUser) => { 6 | const settings = getSettings(); 7 | 8 | const bindings = Array.isArray(settings.keyBindings.findChar) 9 | ? settings.keyBindings.findChar 10 | : [settings.keyBindings.findChar]; 11 | 12 | bindings.forEach((binding, index) => { 13 | logseq.App.registerCommandPalette( 14 | { 15 | key: "vim-shortcut-find-char-" + index, 16 | label: "Find character (f)", 17 | keybinding: { 18 | mode: "non-editing", 19 | binding, 20 | }, 21 | }, 22 | async () => { 23 | debug("Find character"); 24 | const searchStore = useSearchStore(); 25 | searchStore.startCharSearch("f"); 26 | } 27 | ); 28 | }); 29 | }; 30 | -------------------------------------------------------------------------------- /src/keybindings/wordForward.ts: -------------------------------------------------------------------------------- 1 | import { ILSPluginUser } from "@logseq/libs/dist/LSPlugin"; 2 | import { debug, getSettings } from "@/common/funcs"; 3 | import { useSearchStore } from "@/stores/search"; 4 | 5 | export default (logseq: ILSPluginUser) => { 6 | const settings = getSettings(); 7 | 8 | const bindings = Array.isArray(settings.keyBindings.wordForward) 9 | ? settings.keyBindings.wordForward 10 | : [settings.keyBindings.wordForward]; 11 | 12 | bindings.forEach((binding, index) => { 13 | logseq.App.registerCommandPalette( 14 | { 15 | key: "vim-shortcut-word-forward-" + index, 16 | label: "Move to next word (w)", 17 | keybinding: { 18 | mode: "non-editing", 19 | binding, 20 | }, 21 | }, 22 | async () => { 23 | debug("Word forward"); 24 | const searchStore = useSearchStore(); 25 | await searchStore.moveWordForward(); 26 | } 27 | ); 28 | }); 29 | }; 30 | -------------------------------------------------------------------------------- /src/keybindings/wordBackward.ts: -------------------------------------------------------------------------------- 1 | import { ILSPluginUser } from "@logseq/libs/dist/LSPlugin"; 2 | import { debug, getSettings } from "@/common/funcs"; 3 | import { useSearchStore } from "@/stores/search"; 4 | 5 | export default (logseq: ILSPluginUser) => { 6 | const settings = getSettings(); 7 | 8 | const bindings = Array.isArray(settings.keyBindings.wordBackward) 9 | ? settings.keyBindings.wordBackward 10 | : [settings.keyBindings.wordBackward]; 11 | 12 | bindings.forEach((binding, index) => { 13 | logseq.App.registerCommandPalette( 14 | { 15 | key: "vim-shortcut-word-backward-" + index, 16 | label: "Move to previous word (b)", 17 | keybinding: { 18 | mode: "non-editing", 19 | binding, 20 | }, 21 | }, 22 | async () => { 23 | debug("Word backward"); 24 | const searchStore = useSearchStore(); 25 | await searchStore.moveWordBackward(); 26 | } 27 | ); 28 | }); 29 | }; 30 | -------------------------------------------------------------------------------- /src/keybindings/extend.ts: -------------------------------------------------------------------------------- 1 | import { ILSPluginUser } from "@logseq/libs/dist/LSPlugin"; 2 | import { debug, getCurrentBlockUUID, getSettings } from "@/common/funcs"; 3 | 4 | export default (logseq: ILSPluginUser) => { 5 | const settings = getSettings(); 6 | 7 | const bindings = Array.isArray(settings.keyBindings.extend) 8 | ? settings.keyBindings.extend 9 | : [settings.keyBindings.extend]; 10 | 11 | bindings.forEach((binding, index) => { 12 | logseq.App.registerCommandPalette( 13 | { 14 | key: "vim-shortcut-extend-" + index, 15 | label: "Extend block", 16 | keybinding: { 17 | mode: "non-editing", 18 | binding, 19 | }, 20 | }, 21 | async () => { 22 | debug("Extend block"); 23 | 24 | let blockUUID = await getCurrentBlockUUID(); 25 | if (blockUUID) { 26 | await logseq.Editor.setBlockCollapsed(blockUUID, { flag: false }); 27 | } 28 | } 29 | ); 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /src/keybindings/collapse.ts: -------------------------------------------------------------------------------- 1 | import { ILSPluginUser } from "@logseq/libs/dist/LSPlugin"; 2 | import { debug, getCurrentBlockUUID, getSettings } from "@/common/funcs"; 3 | 4 | export default (logseq: ILSPluginUser) => { 5 | const settings = getSettings(); 6 | 7 | const bindings = Array.isArray(settings.keyBindings.collapse) 8 | ? settings.keyBindings.collapse 9 | : [settings.keyBindings.collapse]; 10 | 11 | bindings.forEach((binding, index) => { 12 | logseq.App.registerCommandPalette( 13 | { 14 | key: "vim-shortcut-collapse-" + index, 15 | label: "Collapse block", 16 | keybinding: { 17 | mode: "non-editing", 18 | binding, 19 | }, 20 | }, 21 | async () => { 22 | debug("Collapse block"); 23 | 24 | let blockUUID = await getCurrentBlockUUID(); 25 | if (blockUUID) { 26 | await logseq.Editor.setBlockCollapsed(blockUUID, { flag: true }); 27 | } 28 | } 29 | ); 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /src/keybindings/findCharBackward.ts: -------------------------------------------------------------------------------- 1 | import { ILSPluginUser } from "@logseq/libs/dist/LSPlugin"; 2 | import { debug, getSettings } from "@/common/funcs"; 3 | import { useSearchStore } from "@/stores/search"; 4 | 5 | export default (logseq: ILSPluginUser) => { 6 | const settings = getSettings(); 7 | 8 | const bindings = Array.isArray(settings.keyBindings.findCharBackward) 9 | ? settings.keyBindings.findCharBackward 10 | : [settings.keyBindings.findCharBackward]; 11 | 12 | bindings.forEach((binding, index) => { 13 | logseq.App.registerCommandPalette( 14 | { 15 | key: "vim-shortcut-find-char-backward-" + index, 16 | label: "Find character backward (F)", 17 | keybinding: { 18 | mode: "non-editing", 19 | binding, 20 | }, 21 | }, 22 | async () => { 23 | debug("Find character backward"); 24 | const searchStore = useSearchStore(); 25 | searchStore.startCharSearch("F"); 26 | } 27 | ); 28 | }); 29 | }; 30 | -------------------------------------------------------------------------------- /src/keybindings/repeatCharSearch.ts: -------------------------------------------------------------------------------- 1 | import { ILSPluginUser } from "@logseq/libs/dist/LSPlugin"; 2 | import { debug, getSettings } from "@/common/funcs"; 3 | import { useSearchStore } from "@/stores/search"; 4 | 5 | export default (logseq: ILSPluginUser) => { 6 | const settings = getSettings(); 7 | 8 | const bindings = Array.isArray(settings.keyBindings.repeatCharSearch) 9 | ? settings.keyBindings.repeatCharSearch 10 | : [settings.keyBindings.repeatCharSearch]; 11 | 12 | bindings.forEach((binding, index) => { 13 | logseq.App.registerCommandPalette( 14 | { 15 | key: "vim-shortcut-repeat-char-search-" + index, 16 | label: "Repeat character search (;)", 17 | keybinding: { 18 | mode: "non-editing", 19 | binding, 20 | }, 21 | }, 22 | async () => { 23 | debug("Repeat character search"); 24 | const searchStore = useSearchStore(); 25 | await searchStore.repeatCharSearch(); 26 | } 27 | ); 28 | }); 29 | }; 30 | -------------------------------------------------------------------------------- /src/stores/mark.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from "pinia"; 2 | import { clearMarks, delMark, getMark, getMarks } from "@/common/funcs"; 3 | 4 | export const useMarkStore = defineStore("mark", { 5 | state: () => ({ 6 | visible: false, 7 | title: "Marks", 8 | content: "", 9 | marks: [], 10 | }), 11 | actions: { 12 | toggle() { 13 | this.visible = !this.visible; 14 | }, 15 | close() { 16 | this.visible = false; 17 | }, 18 | async deleteMarks(numbers: string[]) { 19 | for (let number of numbers) { 20 | await delMark(number); 21 | } 22 | }, 23 | getMark(number) { 24 | return getMark(number); 25 | }, 26 | async clearMarks() { 27 | await clearMarks(); 28 | }, 29 | reload() { 30 | const marksObj = getMarks(); 31 | this.marks = Object.keys(marksObj).map((key) => { 32 | return { 33 | ...marksObj[key], 34 | key, 35 | color: marksObj[key].block ? "#b3e19d" : "#66b1ff", 36 | }; 37 | }); 38 | }, 39 | }, 40 | }); 41 | -------------------------------------------------------------------------------- /src/keybindings/repeatCharSearchReverse.ts: -------------------------------------------------------------------------------- 1 | import { ILSPluginUser } from "@logseq/libs/dist/LSPlugin"; 2 | import { debug, getSettings } from "@/common/funcs"; 3 | import { useSearchStore } from "@/stores/search"; 4 | 5 | export default (logseq: ILSPluginUser) => { 6 | const settings = getSettings(); 7 | 8 | const bindings = Array.isArray(settings.keyBindings.repeatCharSearchReverse) 9 | ? settings.keyBindings.repeatCharSearchReverse 10 | : [settings.keyBindings.repeatCharSearchReverse]; 11 | 12 | bindings.forEach((binding, index) => { 13 | logseq.App.registerCommandPalette( 14 | { 15 | key: "vim-shortcut-repeat-char-search-reverse-" + index, 16 | label: "Repeat character search reverse (,)", 17 | keybinding: { 18 | mode: "non-editing", 19 | binding, 20 | }, 21 | }, 22 | async () => { 23 | debug("Repeat character search reverse"); 24 | const searchStore = useSearchStore(); 25 | await searchStore.repeatCharSearchReverse(); 26 | } 27 | ); 28 | }); 29 | }; 30 | -------------------------------------------------------------------------------- /src/keybindings/redo.ts: -------------------------------------------------------------------------------- 1 | import { ILSPluginUser } from "@logseq/libs/dist/LSPlugin"; 2 | import { debug, getNumber, getSettings, resetNumber } from "@/common/funcs"; 3 | 4 | export default (logseq: ILSPluginUser) => { 5 | const settings = getSettings(); 6 | 7 | const bindings = Array.isArray(settings.keyBindings.redo) 8 | ? settings.keyBindings.redo 9 | : [settings.keyBindings.redo]; 10 | 11 | bindings.forEach((binding, index) => { 12 | logseq.App.registerCommandPalette( 13 | { 14 | key: "vim-shortcut-redo-" + index, 15 | label: "Redo", 16 | keybinding: { 17 | mode: "non-editing", 18 | binding, 19 | }, 20 | }, 21 | async () => { 22 | debug("redo"); 23 | 24 | const number = getNumber(); 25 | resetNumber(); 26 | 27 | for (let i = 0; i < number; i++) { 28 | // @ts-ignore 29 | await logseq.App.invokeExternalCommand("logseq.editor/redo"); 30 | await logseq.Editor.exitEditingMode(true); 31 | } 32 | } 33 | ); 34 | }); 35 | }; 36 | -------------------------------------------------------------------------------- /src/keybindings/undo.ts: -------------------------------------------------------------------------------- 1 | import { ILSPluginUser } from "@logseq/libs/dist/LSPlugin"; 2 | import { debug, getNumber, getSettings, resetNumber } from "@/common/funcs"; 3 | 4 | export default (logseq: ILSPluginUser) => { 5 | const settings = getSettings(); 6 | 7 | const bindings = Array.isArray(settings.keyBindings.undo) 8 | ? settings.keyBindings.undo 9 | : [settings.keyBindings.undo]; 10 | 11 | bindings.forEach((binding, index) => { 12 | logseq.App.registerCommandPalette( 13 | { 14 | key: "vim-shortcut-undo-" + index, 15 | label: "Undo", 16 | keybinding: { 17 | mode: "non-editing", 18 | binding, 19 | }, 20 | }, 21 | async () => { 22 | debug("Undo"); 23 | 24 | const number = getNumber(); 25 | resetNumber(); 26 | 27 | for (let i = 0; i < number; i++) { 28 | // @ts-ignore 29 | await logseq.App.invokeExternalCommand("logseq.editor/undo"); 30 | await logseq.Editor.exitEditingMode(true); 31 | } 32 | } 33 | ); 34 | }); 35 | }; 36 | -------------------------------------------------------------------------------- /src/keybindings/toggleVisualMode.ts: -------------------------------------------------------------------------------- 1 | import { ILSPluginUser } from "@logseq/libs/dist/LSPlugin"; 2 | import { 3 | debug, 4 | getSettings, 5 | getVisualMode, 6 | setVisualMode, 7 | } from "@/common/funcs"; 8 | 9 | export default (logseq: ILSPluginUser) => { 10 | const settings = getSettings(); 11 | 12 | const bindings = Array.isArray(settings.keyBindings.toggleVisualMode) 13 | ? settings.keyBindings.toggleVisualMode 14 | : [settings.keyBindings.toggleVisualMode]; 15 | 16 | bindings.forEach((binding, index) => { 17 | logseq.App.registerCommandPalette( 18 | { 19 | key: "vim-shortcut-toggleVisualMode-" + index, 20 | label: "Toggle visual mode", 21 | keybinding: { 22 | mode: "non-editing", 23 | binding, 24 | }, 25 | }, 26 | async () => { 27 | debug("Toggle visual mode"); 28 | 29 | const visualMode = getVisualMode(); 30 | if (visualMode) { 31 | setVisualMode(false); 32 | } else { 33 | setVisualMode(true); 34 | } 35 | } 36 | ); 37 | }); 38 | }; 39 | -------------------------------------------------------------------------------- /src/keybindings/number.ts: -------------------------------------------------------------------------------- 1 | import { ILSPluginUser } from '@logseq/libs/dist/LSPlugin'; 2 | import { debug, setNumber, getNumber, resetNumber } from '@/common/funcs'; 3 | import { useSearchStore } from '@/stores/search'; 4 | 5 | export default (logseq: ILSPluginUser) => { 6 | for (let n of [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) { 7 | logseq.App.registerCommandPalette({ 8 | key: `vim-shortcut-number-${n}`, 9 | label: `${n}`, 10 | keybinding: { 11 | mode: 'non-editing', 12 | binding: `${n}` 13 | } 14 | }, async () => { 15 | debug(`${n}`); 16 | 17 | // Special handling for 0: move to line start if no previous number 18 | if (n === 0) { 19 | const currentNumber = getNumber(); 20 | // If current number is 1 (default), it means no number has been pressed yet 21 | if (currentNumber === 1) { 22 | // Execute line start logic 23 | const searchStore = useSearchStore(); 24 | await searchStore.moveLineStart(); 25 | return; 26 | } 27 | } 28 | 29 | setNumber(n); 30 | }); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/keybindings/changeCaseLowerCase.ts: -------------------------------------------------------------------------------- 1 | import { ILSPluginUser } from "@logseq/libs/dist/LSPlugin"; 2 | import * as cc from "change-case-all"; 3 | import { debug, getSettings } from "@/common/funcs"; 4 | 5 | export default (logseq: ILSPluginUser) => { 6 | const settings = getSettings(); 7 | 8 | const bindings = Array.isArray(settings.keyBindings.changeCaseLower) 9 | ? settings.keyBindings.changeCaseLower 10 | : [settings.keyBindings.changeCaseLower]; 11 | 12 | bindings.forEach((binding, index) => { 13 | logseq.App.registerCommandPalette( 14 | { 15 | key: "vim-shortcut-change-case-lower-" + index, 16 | label: "Change case lower", 17 | keybinding: { 18 | mode: "non-editing", 19 | binding, 20 | }, 21 | }, 22 | async () => { 23 | debug("Change case lower"); 24 | 25 | const block = await logseq.Editor.getCurrentBlock(); 26 | if (block && block.content) { 27 | const content = block.content; 28 | 29 | await logseq.Editor.updateBlock(block.uuid, cc.lowerCase(content)); 30 | } 31 | } 32 | ); 33 | }); 34 | }; 35 | -------------------------------------------------------------------------------- /src/keybindings/changeCaseUpperCase.ts: -------------------------------------------------------------------------------- 1 | import { ILSPluginUser } from "@logseq/libs/dist/LSPlugin"; 2 | import * as cc from "change-case-all"; 3 | import { debug, getSettings } from "@/common/funcs"; 4 | 5 | export default (logseq: ILSPluginUser) => { 6 | const settings = getSettings(); 7 | 8 | const bindings = Array.isArray(settings.keyBindings.changeCaseUpper) 9 | ? settings.keyBindings.changeCaseUpper 10 | : [settings.keyBindings.changeCaseUpper]; 11 | 12 | bindings.forEach((binding, index) => { 13 | logseq.App.registerCommandPalette( 14 | { 15 | key: "vim-shortcut-change-case-upper-" + index, 16 | label: "Change case upper", 17 | keybinding: { 18 | mode: "non-editing", 19 | binding, 20 | }, 21 | }, 22 | async () => { 23 | debug("Change case upper"); 24 | 25 | const block = await logseq.Editor.getCurrentBlock(); 26 | if (block && block.content) { 27 | const content = block.content; 28 | 29 | await logseq.Editor.updateBlock(block.uuid, cc.upperCase(content)); 30 | } 31 | } 32 | ); 33 | }); 34 | }; 35 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import vue from "@vitejs/plugin-vue"; 3 | import AutoImport from "unplugin-auto-import/vite"; 4 | import Components from "unplugin-vue-components/vite"; 5 | import { ElementPlusResolver } from "unplugin-vue-components/resolvers"; 6 | import path from "path"; 7 | 8 | export default defineConfig({ 9 | base: "./", 10 | build: { 11 | sourcemap: false, 12 | target: "esnext", 13 | minify: "esbuild", 14 | chunkSizeWarningLimit: 1024, 15 | rollupOptions: { 16 | output: { 17 | manualChunks: { 18 | logseq: ["@logseq/libs"], 19 | }, 20 | }, 21 | }, 22 | }, 23 | resolve: { 24 | alias: { 25 | "@": path.resolve(__dirname, "./src"), 26 | }, 27 | }, 28 | plugins: [ 29 | vue(), 30 | AutoImport({ 31 | include: [ 32 | /\.[tj]sx?$/, // .ts, .tsx, .js, .jsx 33 | /\.vue$/, 34 | /\.vue\?vue/, // .vue 35 | ], 36 | imports: ["vue"], 37 | resolvers: [ElementPlusResolver()], 38 | }), 39 | Components({ 40 | resolvers: [ElementPlusResolver()], 41 | }), 42 | ], 43 | }); 44 | -------------------------------------------------------------------------------- /src/keybindings/top.ts: -------------------------------------------------------------------------------- 1 | import { ILSPluginUser } from "@logseq/libs/dist/LSPlugin"; 2 | import { 3 | debug, 4 | getCurrentPage, 5 | getSettings, 6 | scrollToBlockInPage, 7 | } from "@/common/funcs"; 8 | 9 | export default (logseq: ILSPluginUser) => { 10 | const settings = getSettings(); 11 | 12 | const bindings = Array.isArray(settings.keyBindings.top) 13 | ? settings.keyBindings.top 14 | : [settings.keyBindings.top]; 15 | 16 | bindings.forEach((binding, index) => { 17 | logseq.App.registerCommandPalette( 18 | { 19 | key: "vim-shortcut-top-" + index, 20 | label: "Go to current page top", 21 | keybinding: { 22 | mode: "non-editing", 23 | binding, 24 | }, 25 | }, 26 | async () => { 27 | debug("top"); 28 | const page = await getCurrentPage(); 29 | if (page?.name) { 30 | const blocks = await logseq.Editor.getPageBlocksTree(page?.name); 31 | if (blocks.length > 0) { 32 | let block = blocks[0]; 33 | scrollToBlockInPage(page.name, block.uuid); 34 | } 35 | } 36 | } 37 | ); 38 | }); 39 | }; 40 | -------------------------------------------------------------------------------- /components.d.ts: -------------------------------------------------------------------------------- 1 | // generated by unplugin-vue-components 2 | // We suggest you to commit this file into source control 3 | // Read more: https://github.com/vuejs/vue-next/pull/3399 4 | 5 | declare module 'vue' { 6 | export interface GlobalComponents { 7 | Color: typeof import('./src/components/Color.vue')['default'] 8 | Command: typeof import('./src/components/Command.vue')['default'] 9 | CopyText: typeof import('./src/components/CopyText.vue')['default'] 10 | ElAutocomplete: typeof import('element-plus/es')['ElAutocomplete'] 11 | ElButton: typeof import('element-plus/es')['ElButton'] 12 | ElDialog: typeof import('element-plus/es')['ElDialog'] 13 | ElDrawer: typeof import('element-plus/es')['ElDrawer'] 14 | ElInput: typeof import('element-plus/es')['ElInput'] 15 | ElTimeline: typeof import('element-plus/es')['ElTimeline'] 16 | ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem'] 17 | Help: typeof import('./src/components/Help.vue')['default'] 18 | Mark: typeof import('./src/components/Mark.vue')['default'] 19 | Search: typeof import('./src/components/Search.vue')['default'] 20 | } 21 | } 22 | 23 | export { } 24 | -------------------------------------------------------------------------------- /src/keybindings/searchBaidu.ts: -------------------------------------------------------------------------------- 1 | import { ILSPluginUser } from "@logseq/libs/dist/LSPlugin"; 2 | import { debug, getCurrentBlockUUID, getSettings } from "@/common/funcs"; 3 | 4 | export default (logseq: ILSPluginUser) => { 5 | const settings = getSettings(); 6 | 7 | const bindings = Array.isArray(settings.keyBindings.searchBaidu) 8 | ? settings.keyBindings.searchBaidu 9 | : [settings.keyBindings.searchBaidu]; 10 | 11 | bindings.forEach((binding, index) => { 12 | logseq.App.registerCommandPalette( 13 | { 14 | key: "vim-shortcut-search-baidu-" + index, 15 | label: "Search in Baidu", 16 | keybinding: { 17 | mode: "non-editing", 18 | binding, 19 | }, 20 | }, 21 | async () => { 22 | debug("Search in Baidu"); 23 | let blockUUID = await getCurrentBlockUUID(); 24 | if (blockUUID) { 25 | let block = await logseq.Editor.getBlock(blockUUID); 26 | if (block?.content) { 27 | await logseq.App.openExternalLink( 28 | `https://www.baidu.com/s?wd=${block.content}` 29 | ); 30 | } 31 | } 32 | } 33 | ); 34 | }); 35 | }; 36 | -------------------------------------------------------------------------------- /src/keybindings/searchGithub.ts: -------------------------------------------------------------------------------- 1 | import { ILSPluginUser } from "@logseq/libs/dist/LSPlugin"; 2 | import { debug, getCurrentBlockUUID, getSettings } from "@/common/funcs"; 3 | 4 | export default (logseq: ILSPluginUser) => { 5 | const settings = getSettings(); 6 | 7 | const bindings = Array.isArray(settings.keyBindings.searchGithub) 8 | ? settings.keyBindings.searchGithub 9 | : [settings.keyBindings.searchGithub]; 10 | 11 | bindings.forEach((binding, index) => { 12 | logseq.App.registerCommandPalette( 13 | { 14 | key: "vim-shortcut-search-github-" + index, 15 | label: "Search in Github", 16 | keybinding: { 17 | mode: "non-editing", 18 | binding, 19 | }, 20 | }, 21 | async () => { 22 | debug("Search in Github"); 23 | let blockUUID = await getCurrentBlockUUID(); 24 | if (blockUUID) { 25 | let block = await logseq.Editor.getBlock(blockUUID); 26 | if (block?.content) { 27 | await logseq.App.openExternalLink( 28 | `https://github.com/search?q=${block.content}` 29 | ); 30 | } 31 | } 32 | } 33 | ); 34 | }); 35 | }; 36 | -------------------------------------------------------------------------------- /src/keybindings/bottom.ts: -------------------------------------------------------------------------------- 1 | import { ILSPluginUser } from "@logseq/libs/dist/LSPlugin"; 2 | import { 3 | debug, 4 | getCurrentPage, 5 | getSettings, 6 | scrollToBlockInPage, 7 | } from "@/common/funcs"; 8 | 9 | export default (logseq: ILSPluginUser) => { 10 | const settings = getSettings(); 11 | 12 | const bindings = Array.isArray(settings.keyBindings.bottom) 13 | ? settings.keyBindings.bottom 14 | : [settings.keyBindings.bottom]; 15 | 16 | bindings.forEach((binding, index) => { 17 | logseq.App.registerCommandPalette( 18 | { 19 | key: "vim-shortcut-bottom-" + index, 20 | label: "Go to current page bottom", 21 | keybinding: { 22 | mode: "non-editing", 23 | binding, 24 | }, 25 | }, 26 | async () => { 27 | debug("bottom"); 28 | const page = await getCurrentPage(); 29 | if (page?.name) { 30 | const blocks = await logseq.Editor.getPageBlocksTree(page?.name); 31 | if (blocks.length > 0) { 32 | let block = blocks[blocks.length - 1]; 33 | scrollToBlockInPage(page.name, block.uuid); 34 | } 35 | } 36 | } 37 | ); 38 | }); 39 | }; 40 | -------------------------------------------------------------------------------- /src/keybindings/searchGoogle.ts: -------------------------------------------------------------------------------- 1 | import { ILSPluginUser } from "@logseq/libs/dist/LSPlugin"; 2 | import { debug, getCurrentBlockUUID, getSettings } from "@/common/funcs"; 3 | 4 | export default (logseq: ILSPluginUser) => { 5 | const settings = getSettings(); 6 | 7 | const bindings = Array.isArray(settings.keyBindings.searchGoogle) 8 | ? settings.keyBindings.searchGoogle 9 | : [settings.keyBindings.searchGoogle]; 10 | 11 | bindings.forEach((binding, index) => { 12 | logseq.App.registerCommandPalette( 13 | { 14 | key: "vim-shortcut-search-google-" + index, 15 | label: "Search in Google", 16 | keybinding: { 17 | mode: "non-editing", 18 | binding, 19 | }, 20 | }, 21 | async () => { 22 | debug("Search in Google"); 23 | let blockUUID = await getCurrentBlockUUID(); 24 | if (blockUUID) { 25 | let block = await logseq.Editor.getBlock(blockUUID); 26 | if (block?.content) { 27 | await logseq.App.openExternalLink( 28 | `https://www.google.com/search?q=${block.content}` 29 | ); 30 | } 31 | } 32 | } 33 | ); 34 | }); 35 | }; 36 | -------------------------------------------------------------------------------- /src/keybindings/searchWikipedia.ts: -------------------------------------------------------------------------------- 1 | import { ILSPluginUser } from "@logseq/libs/dist/LSPlugin"; 2 | import { debug, getCurrentBlockUUID, getSettings } from "@/common/funcs"; 3 | 4 | export default (logseq: ILSPluginUser) => { 5 | const settings = getSettings(); 6 | 7 | const bindings = Array.isArray(settings.keyBindings.searchWikipedia) 8 | ? settings.keyBindings.searchWikipedia 9 | : [settings.keyBindings.searchWikipedia]; 10 | 11 | bindings.forEach((binding, index) => { 12 | logseq.App.registerCommandPalette( 13 | { 14 | key: "vim-shortcut-search-wikipedia-" + index, 15 | label: "Search in Wikipedia", 16 | keybinding: { 17 | mode: "non-editing", 18 | binding, 19 | }, 20 | }, 21 | async () => { 22 | debug("Search in Wikipedia"); 23 | let blockUUID = await getCurrentBlockUUID(); 24 | if (blockUUID) { 25 | let block = await logseq.Editor.getBlock(blockUUID); 26 | if (block?.content) { 27 | await logseq.App.openExternalLink( 28 | `https://en.wikipedia.org/wiki/${block.content}` 29 | ); 30 | } 31 | } 32 | } 33 | ); 34 | }); 35 | }; 36 | -------------------------------------------------------------------------------- /src/keybindings/searchYoutube.ts: -------------------------------------------------------------------------------- 1 | import { ILSPluginUser } from "@logseq/libs/dist/LSPlugin"; 2 | import { debug, getCurrentBlockUUID, getSettings } from "@/common/funcs"; 3 | 4 | export default (logseq: ILSPluginUser) => { 5 | const settings = getSettings(); 6 | 7 | const bindings = Array.isArray(settings.keyBindings.searchYoutube) 8 | ? settings.keyBindings.searchYoutube 9 | : [settings.keyBindings.searchYoutube]; 10 | 11 | bindings.forEach((binding, index) => { 12 | logseq.App.registerCommandPalette( 13 | { 14 | key: "vim-shortcut-search-youtube-" + index, 15 | label: "Search in Youtube", 16 | keybinding: { 17 | mode: "non-editing", 18 | binding, 19 | }, 20 | }, 21 | async () => { 22 | debug("Search in Youtube"); 23 | let blockUUID = await getCurrentBlockUUID(); 24 | if (blockUUID) { 25 | let block = await logseq.Editor.getBlock(blockUUID); 26 | if (block?.content) { 27 | await logseq.App.openExternalLink( 28 | `https://www.youtube.com/results?search_query=${block.content}` 29 | ); 30 | } 31 | } 32 | } 33 | ); 34 | }); 35 | }; 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "logseq-plugin-vim-shortcuts", 3 | "version": "0.1.24", 4 | "schemaVersion": "1.0.0", 5 | "license": "MIT", 6 | "main": "index.html", 7 | "logseq": { 8 | "icon": "./icon.png", 9 | "id": "logseq-plugin-vim-shortcuts", 10 | "title": "Vim Shortcuts" 11 | }, 12 | "scripts": { 13 | "build": "vite build && cp icon.png package.json LICENSE dist/" 14 | }, 15 | "devDependencies": { 16 | "@types/lodash-es": "*", 17 | "@types/minimist": "^1.2.5", 18 | "@vitejs/plugin-vue": "^2.2.4", 19 | "autoprefixer": "^10.4.23", 20 | "postcss": "^8.5.6", 21 | "tailwindcss": "^3.0.23", 22 | "typescript": "^4.6.2", 23 | "unplugin-auto-import": "^0.6.1", 24 | "unplugin-vue-components": "^0.17.21", 25 | "vite": "^2.8.6" 26 | }, 27 | "dependencies": { 28 | "@joeattardi/emoji-button": "^4.6.4", 29 | "@logseq/libs": "^0.0.17", 30 | "change-case-all": "^2.1.0", 31 | "clipboardy": "^5.0.2", 32 | "date-fns": "^4.1.0", 33 | "element-plus": "^2.13.0", 34 | "hotkeys-js": "^4.0.0", 35 | "lorem-ipsum": "^2.0.8", 36 | "minimist": "^1.2.8", 37 | "pinia": "^2.0.11", 38 | "vue": "^3.5.26", 39 | "vue-color-kit": "^1.0.6" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/keybindings/emoji.ts: -------------------------------------------------------------------------------- 1 | import { debug, getSettings, showMainUI } from "@/common/funcs"; 2 | import { useEmojiStore } from "@/stores/emoji"; 3 | import { ILSPluginUser } from "@logseq/libs/dist/LSPlugin"; 4 | 5 | export default (logseq: ILSPluginUser) => { 6 | const settings = getSettings(); 7 | 8 | const bindings = Array.isArray(settings.keyBindings.emoji) 9 | ? settings.keyBindings.emoji 10 | : [settings.keyBindings.emoji]; 11 | 12 | const emojiHandler = async () => { 13 | debug("Insert emoji"); 14 | 15 | showMainUI(false); 16 | 17 | const isEditing = await logseq.Editor.checkEditing(); 18 | if (!isEditing) { 19 | logseq.UI.showMsg("Please edit a block first."); 20 | return; 21 | } 22 | const emojiStore = useEmojiStore(); 23 | emojiStore.showPicker(); 24 | }; 25 | 26 | bindings.forEach((binding, index) => { 27 | logseq.App.registerCommandPalette( 28 | { 29 | key: "vim-shortcut-emoji-" + index, 30 | label: "Insert emoji", 31 | keybinding: { 32 | mode: "global", 33 | binding, 34 | }, 35 | }, 36 | emojiHandler 37 | ); 38 | }); 39 | 40 | logseq.Editor.registerSlashCommand("Insert Emoji", emojiHandler); 41 | }; 42 | -------------------------------------------------------------------------------- /src/keybindings/searchStackoverflow.ts: -------------------------------------------------------------------------------- 1 | import { ILSPluginUser } from "@logseq/libs/dist/LSPlugin"; 2 | import { debug, getCurrentBlockUUID, getSettings } from "@/common/funcs"; 3 | 4 | export default (logseq: ILSPluginUser) => { 5 | const settings = getSettings(); 6 | 7 | const bindings = Array.isArray(settings.keyBindings.searchStackoverflow) 8 | ? settings.keyBindings.searchStackoverflow 9 | : [settings.keyBindings.searchStackoverflow]; 10 | 11 | bindings.forEach((binding, index) => { 12 | logseq.App.registerCommandPalette( 13 | { 14 | key: "vim-shortcut-search-stackoverflow-" + index, 15 | label: "Search in Stackoverflow", 16 | keybinding: { 17 | mode: "non-editing", 18 | binding, 19 | }, 20 | }, 21 | async () => { 22 | debug("Search in Stackoverflow"); 23 | let blockUUID = await getCurrentBlockUUID(); 24 | if (blockUUID) { 25 | let block = await logseq.Editor.getBlock(blockUUID); 26 | if (block?.content) { 27 | await logseq.App.openExternalLink( 28 | `http://stackoverflow.com/search?q=${block.content}` 29 | ); 30 | } 31 | } 32 | } 33 | ); 34 | }); 35 | }; 36 | -------------------------------------------------------------------------------- /src/keybindings/pastePrev.ts: -------------------------------------------------------------------------------- 1 | import { ILSPluginUser } from "@logseq/libs/dist/LSPlugin"; 2 | import { 3 | debug, 4 | getCurrentBlockUUID, 5 | getSettings, 6 | readClipboard, 7 | } from "@/common/funcs"; 8 | 9 | export default (logseq: ILSPluginUser) => { 10 | const settings = getSettings(); 11 | 12 | const bindings = Array.isArray(settings.keyBindings.pastePrev) 13 | ? settings.keyBindings.pastePrev 14 | : [settings.keyBindings.pastePrev]; 15 | 16 | bindings.forEach((binding, index) => { 17 | logseq.App.registerCommandPalette( 18 | { 19 | key: "vim-shortcut-paste-prev-" + index, 20 | label: "Paste to prev block", 21 | keybinding: { 22 | mode: "non-editing", 23 | binding, 24 | }, 25 | }, 26 | async () => { 27 | debug("Paste to prev block"); 28 | let blockUUID = await getCurrentBlockUUID(); 29 | if (blockUUID) { 30 | let block = await logseq.Editor.getBlock(blockUUID); 31 | if (block?.uuid) { 32 | await logseq.Editor.insertBlock(block.uuid, readClipboard(), { 33 | before: true, 34 | sibling: true, 35 | }); 36 | } 37 | } 38 | } 39 | ); 40 | }); 41 | }; 42 | -------------------------------------------------------------------------------- /src/keybindings/pasteNext.ts: -------------------------------------------------------------------------------- 1 | import { ILSPluginUser } from "@logseq/libs/dist/LSPlugin"; 2 | import { 3 | debug, 4 | getCurrentBlockUUID, 5 | getSettings, 6 | readClipboard, 7 | } from "@/common/funcs"; 8 | 9 | export default (logseq: ILSPluginUser) => { 10 | const settings = getSettings(); 11 | 12 | const bindings = Array.isArray(settings.keyBindings.pasteNext) 13 | ? settings.keyBindings.pasteNext 14 | : [settings.keyBindings.pasteNext]; 15 | 16 | bindings.forEach((binding, index) => { 17 | logseq.App.registerCommandPalette( 18 | { 19 | key: "vim-shortcut-paste-next-" + index, 20 | label: "Paste to next block", 21 | keybinding: { 22 | mode: "non-editing", 23 | binding, 24 | }, 25 | }, 26 | async () => { 27 | debug("Paste to next block"); 28 | 29 | let blockUUID = await getCurrentBlockUUID(); 30 | if (blockUUID) { 31 | let block = await logseq.Editor.getBlock(blockUUID); 32 | if (block?.uuid) { 33 | await logseq.Editor.insertBlock(block.uuid, readClipboard(), { 34 | before: false, 35 | sibling: true, 36 | }); 37 | } 38 | } 39 | } 40 | ); 41 | }); 42 | }; 43 | -------------------------------------------------------------------------------- /src/keybindings/copyCurrentBlockContent.ts: -------------------------------------------------------------------------------- 1 | import { ILSPluginUser } from "@logseq/libs/dist/LSPlugin"; 2 | import { 3 | debug, 4 | getCurrentBlockUUID, 5 | getSettings, 6 | writeClipboard, 7 | } from "@/common/funcs"; 8 | 9 | export default (logseq: ILSPluginUser) => { 10 | const settings = getSettings(); 11 | 12 | const bindings = Array.isArray(settings.keyBindings.copyCurrentBlockContent) 13 | ? settings.keyBindings.copyCurrentBlockContent 14 | : [settings.keyBindings.copyCurrentBlockContent]; 15 | 16 | bindings.forEach((binding, index) => { 17 | logseq.App.registerCommandPalette( 18 | { 19 | key: "vim-shortcut-copy-current-block-content-" + index, 20 | label: "Copy current block content", 21 | keybinding: { 22 | mode: "non-editing", 23 | binding, 24 | }, 25 | }, 26 | async () => { 27 | debug("Copy current block contents"); 28 | let blockUUID = await getCurrentBlockUUID(); 29 | if (blockUUID) { 30 | const block = await logseq.Editor.getBlock(blockUUID); 31 | if (block?.content) { 32 | const { content } = block; 33 | writeClipboard(content); 34 | } 35 | } 36 | } 37 | ); 38 | }); 39 | }; 40 | -------------------------------------------------------------------------------- /src/keybindings/prevNewBlock.ts: -------------------------------------------------------------------------------- 1 | import { ILSPluginUser } from "@logseq/libs/dist/LSPlugin"; 2 | import { debug, getCurrentBlockUUID, getSettings } from "@/common/funcs"; 3 | 4 | export default (logseq: ILSPluginUser) => { 5 | const settings = getSettings(); 6 | 7 | const bindings = Array.isArray(settings.keyBindings.prevNewBlock) 8 | ? settings.keyBindings.prevNewBlock 9 | : [settings.keyBindings.prevNewBlock]; 10 | 11 | bindings.forEach((binding, index) => { 12 | logseq.App.registerCommandPalette( 13 | { 14 | key: "vim-shortcut-prev-new-block-" + index, 15 | label: "Create new prev block", 16 | keybinding: { 17 | mode: "non-editing", 18 | binding, 19 | }, 20 | }, 21 | async () => { 22 | debug("Create new prev block"); 23 | let blockUUID = await getCurrentBlockUUID(); 24 | if (blockUUID) { 25 | let block = await logseq.Editor.getBlock(blockUUID); 26 | if (block?.uuid) { 27 | const newBlock = await logseq.Editor.insertBlock(block.uuid, "", { 28 | before: true, 29 | sibling: true, 30 | }); 31 | 32 | if (newBlock?.uuid) { 33 | await logseq.Editor.editBlock(newBlock.uuid); 34 | } 35 | } 36 | } 37 | } 38 | ); 39 | }); 40 | }; 41 | -------------------------------------------------------------------------------- /src/keybindings/cut.ts: -------------------------------------------------------------------------------- 1 | import { ILSPluginUser } from "@logseq/libs/dist/LSPlugin"; 2 | import { debug, getNumber, getSettings, resetNumber } from "@/common/funcs"; 3 | 4 | export default (logseq: ILSPluginUser) => { 5 | const settings = getSettings(); 6 | 7 | const bindings = Array.isArray(settings.keyBindings.cut) 8 | ? settings.keyBindings.cut 9 | : [settings.keyBindings.cut]; 10 | 11 | bindings.forEach((binding, index) => { 12 | logseq.App.registerCommandPalette( 13 | { 14 | key: "vim-shortcut-cut-" + index, 15 | label: "Cut", 16 | keybinding: { 17 | mode: "non-editing", 18 | binding, 19 | }, 20 | }, 21 | async () => { 22 | debug("cut"); 23 | 24 | const selected = await logseq.Editor.getSelectedBlocks(); 25 | if (selected.length > 1) { 26 | for (let block of selected) { 27 | const content = block.content.substring(1); 28 | 29 | await logseq.Editor.updateBlock(block.uuid, content); 30 | } 31 | } else { 32 | const block = await logseq.Editor.getCurrentBlock(); 33 | if (block) { 34 | const content = block.content.substring(1); 35 | 36 | await logseq.Editor.updateBlock(block.uuid, content); 37 | } 38 | } 39 | } 40 | ); 41 | }); 42 | }; 43 | -------------------------------------------------------------------------------- /src/keybindings/nextNewBlock.ts: -------------------------------------------------------------------------------- 1 | import { ILSPluginUser } from "@logseq/libs/dist/LSPlugin"; 2 | import { debug, getCurrentBlockUUID, getSettings } from "@/common/funcs"; 3 | 4 | export default (logseq: ILSPluginUser) => { 5 | const settings = getSettings(); 6 | 7 | const bindings = Array.isArray(settings.keyBindings.nextNewBlock) 8 | ? settings.keyBindings.nextNewBlock 9 | : [settings.keyBindings.nextNewBlock]; 10 | 11 | bindings.forEach((binding, index) => { 12 | logseq.App.registerCommandPalette( 13 | { 14 | key: "vim-shortcut-next-new-block-" + index, 15 | label: "Create new next block", 16 | keybinding: { 17 | mode: "non-editing", 18 | binding, 19 | }, 20 | }, 21 | async () => { 22 | debug("create new next block"); 23 | let blockUUID = await getCurrentBlockUUID(); 24 | if (blockUUID) { 25 | let block = await logseq.Editor.getBlock(blockUUID); 26 | if (block?.uuid) { 27 | const newBlock = await logseq.Editor.insertBlock(block.uuid, "", { 28 | before: false, 29 | sibling: true, 30 | }); 31 | 32 | if (newBlock?.uuid) { 33 | await logseq.Editor.editBlock(newBlock.uuid); 34 | } 35 | } 36 | } 37 | } 38 | ); 39 | }); 40 | }; 41 | -------------------------------------------------------------------------------- /src/components/CopyText.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/commands/emoji.ts: -------------------------------------------------------------------------------- 1 | import "@logseq/libs"; 2 | import { useEmojiStore } from "@/stores/emoji"; 3 | import { hideMainUI } from "@/common/funcs"; 4 | import { useCommandStore } from "@/stores/command"; 5 | 6 | export async function generate(argv) { 7 | if (argv._.length < 1 || !argv._[0]) { 8 | // logseq.UI.showMsg("Please input at least one emoji."); 9 | // popup emoji picker 10 | const isEditing = await logseq.Editor.checkEditing(); 11 | if (!isEditing) { 12 | logseq.UI.showMsg("Please edit a block first."); 13 | return; 14 | } 15 | const emojiStore = useEmojiStore(); 16 | emojiStore.showPicker(); 17 | const commandStore = useCommandStore(); 18 | commandStore.hide(); 19 | 20 | return; 21 | } 22 | let repeats = 1; 23 | if (Number.isInteger(parseInt(argv._[argv._.length - 1]))) { 24 | repeats = parseInt(argv._[argv._.length - 1]); 25 | argv._.pop(); 26 | } 27 | 28 | if (argv._.length < 1) { 29 | logseq.UI.showMsg("Please input at least one emoji."); 30 | return; 31 | } 32 | 33 | for (let i = 0; i < argv._.length; i++) { 34 | const char = argv._[i]; 35 | const charRepeats = [...new Array(repeats)].map(() => char).join(""); 36 | await logseq.Editor.insertAtEditingCursor(charRepeats); 37 | 38 | if (argv.space) { 39 | await logseq.Editor.insertAtEditingCursor(" "); 40 | } 41 | } 42 | hideMainUI(); 43 | } 44 | -------------------------------------------------------------------------------- /src/commands/lorem.ts: -------------------------------------------------------------------------------- 1 | import "@logseq/libs"; 2 | import { loremIpsum } from "lorem-ipsum"; 3 | export async function generate(argv) { 4 | let block = await logseq.Editor.getCurrentBlock(); 5 | if (!block) { 6 | logseq.UI.showMsg("No block selected!"); 7 | return; 8 | } 9 | 10 | let u = argv.unit || argv.u; 11 | const uMapping = { 12 | p: "paragraph", 13 | w: "word", 14 | s: "sentence", 15 | }; 16 | if (uMapping[u]) { 17 | u = uMapping[u]; 18 | } 19 | 20 | const lines = parseInt(argv._[0]) || 1; 21 | let unit = ["paragraph", "sentence", "word"].includes(u) ? u : "sentence"; 22 | if (argv.paragraph || argv.p) { 23 | unit = "paragraph"; 24 | } else if (argv.sentence || argv.s) { 25 | unit = "sentence"; 26 | } else if (argv.word || argv.w) { 27 | unit = "word"; 28 | } 29 | 30 | let currentBlockFilled = false; 31 | for (let i = 0; i < lines; i++) { 32 | const text = loremIpsum({ 33 | count: 1, 34 | units: unit, 35 | format: "plain", 36 | sentenceLowerBound: 5, 37 | sentenceUpperBound: 15, 38 | }); 39 | if (!block.content && !currentBlockFilled) { 40 | await logseq.Editor.updateBlock(block.uuid, text); 41 | currentBlockFilled = true; 42 | } else { 43 | await logseq.Editor.insertBlock(block.uuid, text, { 44 | before: false, 45 | sibling: true, 46 | }); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/keybindings/cutWord.ts: -------------------------------------------------------------------------------- 1 | import { ILSPluginUser } from "@logseq/libs/dist/LSPlugin"; 2 | import { debug, getNumber, getSettings, resetNumber } from "@/common/funcs"; 3 | 4 | export default (logseq: ILSPluginUser) => { 5 | const settings = getSettings(); 6 | 7 | const bindings = Array.isArray(settings.keyBindings.cutWord) 8 | ? settings.keyBindings.cutWord 9 | : [settings.keyBindings.cutWord]; 10 | 11 | bindings.forEach((binding, index) => { 12 | logseq.App.registerCommandPalette( 13 | { 14 | key: "vim-shortcut-cut-word-" + index, 15 | label: "Cut word", 16 | keybinding: { 17 | mode: "non-editing", 18 | binding, 19 | }, 20 | }, 21 | async () => { 22 | debug("cut word"); 23 | 24 | const selected = await logseq.Editor.getSelectedBlocks(); 25 | if (selected.length > 1) { 26 | for (let block of selected) { 27 | const content = block.content.split(" ").slice(1).join(" "); 28 | 29 | await logseq.Editor.updateBlock(block.uuid, content); 30 | } 31 | } else { 32 | const block = await logseq.Editor.getCurrentBlock(); 33 | if (block) { 34 | const content = block.content.split(" ").slice(1).join(" "); 35 | 36 | await logseq.Editor.updateBlock(block.uuid, content); 37 | } 38 | } 39 | } 40 | ); 41 | }); 42 | }; 43 | -------------------------------------------------------------------------------- /src/keybindings/copyCurrentBlockRef.ts: -------------------------------------------------------------------------------- 1 | import { ILSPluginUser } from "@logseq/libs/dist/LSPlugin"; 2 | import { 3 | debug, 4 | getCurrentBlockUUID, 5 | getSettings, 6 | writeClipboard, 7 | } from "@/common/funcs"; 8 | 9 | export default (logseq: ILSPluginUser) => { 10 | const settings = getSettings(); 11 | 12 | const bindings = Array.isArray(settings.keyBindings.copyCurrentBlockRef) 13 | ? settings.keyBindings.copyCurrentBlockRef 14 | : [settings.keyBindings.copyCurrentBlockRef]; 15 | 16 | bindings.forEach((binding, index) => { 17 | logseq.App.registerCommandPalette( 18 | { 19 | key: "vim-shortcut-copy-current-block-ref-" + index, 20 | label: "Copy current block ref", 21 | keybinding: { 22 | mode: "non-editing", 23 | binding, 24 | }, 25 | }, 26 | async () => { 27 | debug("Copy current block ref"); 28 | let blockUUID = await getCurrentBlockUUID(); 29 | if (blockUUID) { 30 | let block = await logseq.Editor.getBlock(blockUUID); 31 | 32 | if (block?.uuid) { 33 | if (!block?.properties?.id) { 34 | await logseq.Editor.upsertBlockProperty( 35 | block.uuid, 36 | "id", 37 | block.uuid 38 | ); 39 | } 40 | const ref = `((${block.uuid}))`; 41 | 42 | writeClipboard(ref); 43 | } 44 | } 45 | } 46 | ); 47 | }); 48 | }; 49 | -------------------------------------------------------------------------------- /src/keybindings/highlightFocusOut.ts: -------------------------------------------------------------------------------- 1 | import { ILSPluginUser } from "@logseq/libs/dist/LSPlugin"; 2 | import { 3 | debug, 4 | getCurrentBlockUUID, 5 | getCurrentPage, 6 | getSettings, 7 | scrollToBlockInPage, 8 | } from "@/common/funcs"; 9 | 10 | export default (logseq: ILSPluginUser) => { 11 | const settings = getSettings(); 12 | 13 | const bindings = Array.isArray(settings.keyBindings.highlightFocusOut) 14 | ? settings.keyBindings.highlightFocusOut 15 | : [settings.keyBindings.highlightFocusOut]; 16 | 17 | bindings.forEach((binding, index) => { 18 | logseq.App.registerCommandPalette( 19 | { 20 | key: "vim-shortcut-highlightFocusOut-" + index, 21 | label: "Highlight focus out", 22 | keybinding: { 23 | mode: "non-editing", 24 | binding, 25 | }, 26 | }, 27 | async () => { 28 | debug("Highlight focus out"); 29 | 30 | const page = await getCurrentPage(); 31 | if (page?.name) { 32 | let blockUUID = await getCurrentBlockUUID(); 33 | if (blockUUID) { 34 | let block = await logseq.Editor.getBlock(blockUUID); 35 | if (block?.parent.id) { 36 | const parentBlock = await logseq.Editor.getBlock( 37 | block?.parent.id 38 | ); 39 | if (parentBlock?.uuid) { 40 | scrollToBlockInPage(page.name, parentBlock?.uuid); 41 | } 42 | } 43 | } 44 | } 45 | } 46 | ); 47 | }); 48 | }; 49 | -------------------------------------------------------------------------------- /src/keybindings/extendAll.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BlockEntity, 3 | BlockUUID, 4 | ILSPluginUser, 5 | } from "@logseq/libs/dist/LSPlugin"; 6 | import { debug, getCurrentBlockUUID, getSettings } from "@/common/funcs"; 7 | 8 | const extend = async (blockUUID: BlockUUID | undefined) => { 9 | if (blockUUID) { 10 | try { 11 | await logseq.Editor.setBlockCollapsed(blockUUID, { flag: false }); 12 | } catch (e) {} 13 | 14 | const block = await logseq.Editor.getBlock(blockUUID, { 15 | includeChildren: true, 16 | }); 17 | 18 | if (block && block.children && block.children.length > 0) { 19 | for (let item of block.children as BlockEntity[]) { 20 | if (item.uuid) { 21 | await extend(item.uuid); 22 | } 23 | } 24 | } 25 | } 26 | }; 27 | 28 | export default (logseq: ILSPluginUser) => { 29 | const settings = getSettings(); 30 | 31 | const bindings = Array.isArray(settings.keyBindings.extendAll) 32 | ? settings.keyBindings.extendAll 33 | : [settings.keyBindings.extendAll]; 34 | 35 | bindings.forEach((binding, index) => { 36 | logseq.App.registerCommandPalette( 37 | { 38 | key: "vim-shortcut-extend-hierarchically-" + index, 39 | label: "Extend block hierarchically", 40 | keybinding: { 41 | mode: "non-editing", 42 | binding, 43 | }, 44 | }, 45 | async () => { 46 | debug("Extend block hierarchically"); 47 | 48 | let blockUUID = await getCurrentBlockUUID(); 49 | await extend(blockUUID); 50 | } 51 | ); 52 | }); 53 | }; 54 | -------------------------------------------------------------------------------- /src/keybindings/collapseAll.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BlockEntity, 3 | BlockUUID, 4 | ILSPluginUser, 5 | } from "@logseq/libs/dist/LSPlugin"; 6 | import { debug, getCurrentBlockUUID, getSettings } from "@/common/funcs"; 7 | 8 | const collapse = async (blockUUID: BlockUUID | undefined) => { 9 | if (blockUUID) { 10 | try { 11 | await logseq.Editor.setBlockCollapsed(blockUUID, { flag: true }); 12 | } catch (e) {} 13 | 14 | const block = await logseq.Editor.getBlock(blockUUID, { 15 | includeChildren: true, 16 | }); 17 | if (block && block.children && block.children.length > 0) { 18 | for (let item of block.children as BlockEntity[]) { 19 | if (item.uuid) { 20 | await collapse(item.uuid); 21 | } 22 | } 23 | } 24 | } 25 | }; 26 | 27 | export default (logseq: ILSPluginUser) => { 28 | const settings = getSettings(); 29 | 30 | const bindings = Array.isArray(settings.keyBindings.collapseAll) 31 | ? settings.keyBindings.collapseAll 32 | : [settings.keyBindings.collapseAll]; 33 | 34 | bindings.forEach((binding, index) => { 35 | logseq.App.registerCommandPalette( 36 | { 37 | key: "vim-shortcut-collapse-hierarchically-" + index, 38 | label: "Collapse block hierarchically", 39 | keybinding: { 40 | mode: "non-editing", 41 | binding, 42 | }, 43 | }, 44 | async () => { 45 | debug("Collapse block hierarchically"); 46 | 47 | let blockUUID = await getCurrentBlockUUID(); 48 | await collapse(blockUUID); 49 | } 50 | ); 51 | }); 52 | }; 53 | -------------------------------------------------------------------------------- /src/keybindings/decrease.ts: -------------------------------------------------------------------------------- 1 | import { ILSPluginUser } from "@logseq/libs/dist/LSPlugin"; 2 | import { debug, getNumber, getSettings, resetNumber } from "@/common/funcs"; 3 | 4 | export default (logseq: ILSPluginUser) => { 5 | const settings = getSettings(); 6 | 7 | const bindings = Array.isArray(settings.keyBindings.decrease) 8 | ? settings.keyBindings.decrease 9 | : [settings.keyBindings.decrease]; 10 | 11 | bindings.forEach((binding, index) => { 12 | logseq.App.registerCommandPalette( 13 | { 14 | key: "vim-shortcut-decrease-" + index, 15 | label: "Decrease", 16 | keybinding: { 17 | mode: "non-editing", 18 | binding, 19 | }, 20 | }, 21 | async () => { 22 | debug("decrease"); 23 | 24 | const number = getNumber(); 25 | resetNumber(); 26 | 27 | const selected = await logseq.Editor.getSelectedBlocks(); 28 | if (selected.length > 1) { 29 | for (let block of selected) { 30 | const content = block.content.replace(/-?[0-9]+/, (match, p1) => { 31 | return `${parseInt(match) - number}`; 32 | }); 33 | 34 | await logseq.Editor.updateBlock(block.uuid, content); 35 | } 36 | } else { 37 | const block = await logseq.Editor.getCurrentBlock(); 38 | if (block) { 39 | const content = block.content.replace(/-?[0-9]+/, (match, p1) => { 40 | return `${parseInt(match) - number}`; 41 | }); 42 | 43 | await logseq.Editor.updateBlock(block.uuid, content); 44 | } 45 | } 46 | } 47 | ); 48 | }); 49 | }; 50 | -------------------------------------------------------------------------------- /src/keybindings/increase.ts: -------------------------------------------------------------------------------- 1 | import { ILSPluginUser } from "@logseq/libs/dist/LSPlugin"; 2 | import { debug, getNumber, getSettings, resetNumber } from "@/common/funcs"; 3 | 4 | export default (logseq: ILSPluginUser) => { 5 | const settings = getSettings(); 6 | 7 | const bindings = Array.isArray(settings.keyBindings.increase) 8 | ? settings.keyBindings.increase 9 | : [settings.keyBindings.increase]; 10 | 11 | bindings.forEach((binding, index) => { 12 | logseq.App.registerCommandPalette( 13 | { 14 | key: "vim-shortcut-increase-" + index, 15 | label: "Increase", 16 | keybinding: { 17 | mode: "non-editing", 18 | binding, 19 | }, 20 | }, 21 | async () => { 22 | debug("increase"); 23 | 24 | const number = getNumber(); 25 | resetNumber(); 26 | 27 | const selected = await logseq.Editor.getSelectedBlocks(); 28 | if (selected.length > 1) { 29 | for (let block of selected) { 30 | const content = block.content.replace(/-?[0-9]+/, (match, p1) => { 31 | return `${parseInt(match) + number}`; 32 | }); 33 | 34 | await logseq.Editor.updateBlock(block.uuid, content); 35 | } 36 | } else { 37 | const block = await logseq.Editor.getCurrentBlock(); 38 | if (block) { 39 | const content = block.content.replace(/-?[0-9]+/, (match, p1) => { 40 | return `${parseInt(match) + number}`; 41 | }); 42 | 43 | await logseq.Editor.updateBlock(block.uuid, content); 44 | } 45 | } 46 | } 47 | ); 48 | }); 49 | }; 50 | -------------------------------------------------------------------------------- /src/keybindings/highlightFocusIn.ts: -------------------------------------------------------------------------------- 1 | import { ILSPluginUser } from "@logseq/libs/dist/LSPlugin"; 2 | import { 3 | debug, 4 | getCurrentBlockUUID, 5 | getCurrentPage, 6 | getSettings, 7 | scrollToBlockInPage, 8 | } from "@/common/funcs"; 9 | 10 | export default (logseq: ILSPluginUser) => { 11 | const settings = getSettings(); 12 | 13 | const bindings = Array.isArray(settings.keyBindings.highlightFocusIn) 14 | ? settings.keyBindings.highlightFocusIn 15 | : [settings.keyBindings.highlightFocusIn]; 16 | 17 | bindings.forEach((binding, index) => { 18 | logseq.App.registerCommandPalette( 19 | { 20 | key: "vim-shortcut-highlightFocusIn-" + index, 21 | label: "Highlight focus in", 22 | keybinding: { 23 | mode: "non-editing", 24 | binding, 25 | }, 26 | }, 27 | async () => { 28 | debug("Highlight focus in"); 29 | 30 | const page = await getCurrentPage(); 31 | if (page?.name) { 32 | let blockUUID = await getCurrentBlockUUID(); 33 | if (blockUUID) { 34 | let block = await logseq.Editor.getBlock(blockUUID, { 35 | includeChildren: true, 36 | }); 37 | if (block?.children && block?.children?.length > 0) { 38 | let focusInBlock = block.children[block.children.length - 1]; 39 | if (Array.isArray(focusInBlock) && focusInBlock[0] === "uuid") { 40 | scrollToBlockInPage(page.name, focusInBlock[1]); 41 | } else if (focusInBlock["uuid"]) { 42 | scrollToBlockInPage(page.name, focusInBlock["uuid"]); 43 | } 44 | } 45 | } 46 | } 47 | } 48 | ); 49 | }); 50 | }; 51 | -------------------------------------------------------------------------------- /src/keybindings/up.ts: -------------------------------------------------------------------------------- 1 | import { ILSPluginUser } from "@logseq/libs/dist/LSPlugin"; 2 | import { 3 | debug, 4 | getNumber, 5 | getSettings, 6 | getVisualMode, 7 | resetNumber, 8 | } from "@/common/funcs"; 9 | import { useSearchStore } from "@/stores/search"; 10 | 11 | export default (logseq: ILSPluginUser) => { 12 | const settings = getSettings(); 13 | 14 | const bindings = Array.isArray(settings.keyBindings.up) 15 | ? settings.keyBindings.up 16 | : [settings.keyBindings.up]; 17 | 18 | bindings.forEach((binding, index) => { 19 | logseq.App.registerCommandPalette( 20 | { 21 | key: "vim-shortcut-up-" + index, 22 | label: "up or move cursor up", 23 | keybinding: { 24 | mode: "non-editing", 25 | binding, 26 | }, 27 | }, 28 | async () => { 29 | const searchStore = useSearchStore(); 30 | 31 | // If in cursor mode, move cursor up (to previous block) 32 | if (searchStore.cursorMode) { 33 | debug("Move cursor up"); 34 | await searchStore.moveCursorUp(); 35 | return; 36 | } 37 | 38 | const number = getNumber(); 39 | resetNumber(); 40 | 41 | const visualMode = getVisualMode(); 42 | 43 | if (visualMode) { 44 | debug("Select up"); 45 | for (let i = 0; i < number; i++) { 46 | await logseq.App.invokeExternalCommand( 47 | // @ts-ignore 48 | "logseq.editor/select-block-up" 49 | ); 50 | } 51 | } else { 52 | debug("Up"); 53 | for (let i = 0; i < number; i++) { 54 | // @ts-ignore 55 | await logseq.App.invokeExternalCommand("logseq.editor/up"); 56 | } 57 | } 58 | } 59 | ); 60 | }); 61 | }; 62 | -------------------------------------------------------------------------------- /src/keybindings/down.ts: -------------------------------------------------------------------------------- 1 | import { ILSPluginUser } from "@logseq/libs/dist/LSPlugin"; 2 | import { 3 | debug, 4 | getNumber, 5 | getSettings, 6 | getVisualMode, 7 | resetNumber, 8 | } from "@/common/funcs"; 9 | import { useSearchStore } from "@/stores/search"; 10 | 11 | export default (logseq: ILSPluginUser) => { 12 | const settings = getSettings(); 13 | 14 | const bindings = Array.isArray(settings.keyBindings.down) 15 | ? settings.keyBindings.down 16 | : [settings.keyBindings.down]; 17 | 18 | bindings.forEach((binding, index) => { 19 | logseq.App.registerCommandPalette( 20 | { 21 | key: "vim-shortcut-down-" + index, 22 | label: "down or move cursor down", 23 | keybinding: { 24 | mode: "non-editing", 25 | binding, 26 | }, 27 | }, 28 | async () => { 29 | const searchStore = useSearchStore(); 30 | 31 | // If in cursor mode, move cursor down (to next block) 32 | if (searchStore.cursorMode) { 33 | debug("Move cursor down"); 34 | await searchStore.moveCursorDown(); 35 | return; 36 | } 37 | 38 | const number = getNumber(); 39 | resetNumber(); 40 | 41 | const visualMode = getVisualMode(); 42 | 43 | if (visualMode) { 44 | debug("Select down"); 45 | for (let i = 0; i < number; i++) { 46 | await logseq.App.invokeExternalCommand( 47 | // @ts-ignore 48 | "logseq.editor/select-block-down" 49 | ); 50 | } 51 | } else { 52 | debug("Down"); 53 | for (let i = 0; i < number; i++) { 54 | // @ts-ignore 55 | await logseq.App.invokeExternalCommand("logseq.editor/down"); 56 | } 57 | } 58 | } 59 | ); 60 | }); 61 | }; 62 | -------------------------------------------------------------------------------- /src/components/Mark.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /src/stores/emoji.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from "pinia"; 2 | import { EmojiButton } from "@joeattardi/emoji-button"; 3 | import { 4 | getNumber, 5 | hideMainUI, 6 | resetNumber, 7 | getSettings, 8 | } from "@/common/funcs"; 9 | import "@logseq/libs"; 10 | 11 | export const useEmojiStore = defineStore("emoji", { 12 | state: () => ({ 13 | picker: null, 14 | emojiPickerEl: null, 15 | }), 16 | actions: { 17 | async makePicker() { 18 | if (this.picker) return this.picker; 19 | 20 | const settings = getSettings(); 21 | const appUserConfig = await logseq.App.getUserConfigs(); 22 | 23 | this.picker = new EmojiButton({ 24 | position: "bottom-start", 25 | theme: appUserConfig.preferredThemeMode, 26 | initialCategory: settings?.showRecentEmojis ? "recents" : null, 27 | }); 28 | 29 | this.picker.on("emoji", async (selection) => { 30 | const number = getNumber(); 31 | resetNumber(); 32 | const emojis = [...new Array(number)] 33 | .map(() => selection.emoji) 34 | .join(""); 35 | await logseq.Editor.insertAtEditingCursor(emojis); 36 | }); 37 | 38 | this.picker.on("hidden", async (selection) => { 39 | hideMainUI(); 40 | }); 41 | 42 | logseq.App.onThemeModeChanged(({ mode }) => { 43 | this.picker.setTheme(mode); 44 | }); 45 | 46 | return this.picker; 47 | }, 48 | async showPicker() { 49 | const { left, top, rect } = 50 | await logseq.Editor.getEditingCursorPosition(); 51 | Object.assign(this.emojiPickerEl.style, { 52 | position: "absolute", 53 | top: top + rect.top + "px", 54 | left: left + rect.left + "px", 55 | }); 56 | 57 | setTimeout(() => { 58 | if (this.picker) { 59 | this.picker.showPicker(this.emojiPickerEl); 60 | } 61 | }, 100); 62 | }, 63 | async initPicker() { 64 | this.emojiPickerEl = document.createElement("div"); 65 | this.emojiPickerEl.classList.add("emoji-picker-trigger"); 66 | document.getElementById("app").appendChild(this.emojiPickerEl); 67 | 68 | this.makePicker(); 69 | }, 70 | }, 71 | }); 72 | -------------------------------------------------------------------------------- /AGENTS.md: -------------------------------------------------------------------------------- 1 | # Agent Guidelines for logseq-plugin-vim-shortcuts 2 | 3 | ## Build/Lint/Test Commands 4 | 5 | - **Build**: `npm run build` or `pnpm build` (builds with Vite and copies assets to dist/) 6 | - **No linting configured** - ensure code follows TypeScript strict mode 7 | - **No testing framework** - manually verify functionality in Logseq 8 | 9 | ## Code Style Guidelines 10 | 11 | ### Imports 12 | 13 | - Group imports: external libraries first, then local imports with `@/` alias 14 | - Use named imports for Logseq APIs: `import { ILSPluginUser } from "@logseq/libs/dist/LSPlugin"` 15 | - Use `@/` alias for src directory imports 16 | 17 | ### Formatting 18 | 19 | - 2-space indentation (configured in .editorconfig) 20 | - Trim trailing whitespace, insert final newlines 21 | - Use single quotes for strings unless containing single quotes 22 | 23 | ### Types & Naming 24 | 25 | - **Variables/Functions**: camelCase (`getSettings`, `handleEnter`) 26 | - **Files**: kebab-case (`command.vue`, `up.ts`) 27 | - **Vue Components**: PascalCase (`Command.vue`) 28 | - **TypeScript**: Use explicit types, avoid `any` except for Logseq APIs 29 | - **Interfaces**: PascalCase with `I` prefix (`ILSPluginUser`) 30 | 31 | ### Vue.js Patterns 32 | 33 | - Use Composition API with ` 6 | 7 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /src/commands/bg.ts: -------------------------------------------------------------------------------- 1 | import "@logseq/libs"; 2 | import { useColorStore } from "@/stores/color"; 3 | import { ParsedArgs } from "minimist"; 4 | import { filterDarkColor } from "@/common/funcs"; 5 | 6 | export const picker = () => { 7 | const colorStore = useColorStore(); 8 | colorStore.show(); 9 | }; 10 | 11 | export const random = async () => { 12 | const colorStore = useColorStore(); 13 | const namedColors = [...Object.keys(colorStore.namedColors)].filter( 14 | (color) => { 15 | const hexColor = colorStore.namedColors[color]; 16 | return filterDarkColor(hexColor); 17 | } 18 | ); 19 | const shuffled = namedColors.sort(() => 0.5 - Math.random()); 20 | const selected = await logseq.Editor.getSelectedBlocks(); 21 | if (selected && selected.length > 1) { 22 | const mapped = selected.map((block, index) => 23 | logseq.Editor.upsertBlockProperty( 24 | block.uuid, 25 | "background-color", 26 | shuffled[index % shuffled.length] 27 | ) 28 | ); 29 | await Promise.all(mapped); 30 | } else { 31 | const block = await logseq.Editor.getCurrentBlock(); 32 | if (block) { 33 | await logseq.Editor.upsertBlockProperty( 34 | block.uuid, 35 | "background-color", 36 | shuffled[0] 37 | ); 38 | // Why this doesn't work? 39 | await logseq.Editor.exitEditingMode(true); 40 | } 41 | } 42 | }; 43 | 44 | export const clear = async () => { 45 | const selected = await logseq.Editor.getSelectedBlocks(); 46 | if (selected && selected.length > 1) { 47 | const mapped = selected.map((block) => 48 | logseq.Editor.upsertBlockProperty(block.uuid, "background-color", null) 49 | ); 50 | await Promise.all(mapped); 51 | } else { 52 | const block = await logseq.Editor.getCurrentBlock(); 53 | if (block) { 54 | await logseq.Editor.upsertBlockProperty( 55 | block.uuid, 56 | "background-color", 57 | null 58 | ); 59 | // Why this doesn't work? 60 | await logseq.Editor.exitEditingMode(true); 61 | } 62 | } 63 | }; 64 | 65 | export const set = async (argv: ParsedArgs) => { 66 | const colorStore = useColorStore(); 67 | const color = argv._[0]; 68 | 69 | const namedColors = colorStore.namedColors; 70 | 71 | if ( 72 | Object.keys(namedColors).includes(color) || 73 | /^#[0-9a-f]{6}$/i.test(color) 74 | ) { 75 | const selected = await logseq.Editor.getSelectedBlocks(); 76 | if (selected && selected.length > 1) { 77 | const mapped = selected.map((block) => 78 | logseq.Editor.upsertBlockProperty(block.uuid, "background-color", color) 79 | ); 80 | await Promise.all(mapped); 81 | } else { 82 | const block = await logseq.Editor.getCurrentBlock(); 83 | if (block) { 84 | await logseq.Editor.upsertBlockProperty( 85 | block.uuid, 86 | "background-color", 87 | color 88 | ); 89 | // Why this doesn't work? 90 | await logseq.Editor.exitEditingMode(true); 91 | } 92 | } 93 | } 94 | }; 95 | -------------------------------------------------------------------------------- /src/keybindings/sort.ts: -------------------------------------------------------------------------------- 1 | import { BlockEntity, ILSPluginUser } from "@logseq/libs/dist/LSPlugin"; 2 | 3 | export default (logseq: ILSPluginUser) => { 4 | const sortHandler = async () => { 5 | const curBlock = await logseq.Editor.getCurrentBlock(); 6 | const selected = await logseq.Editor.getSelectedBlocks(); 7 | const isEditing = await logseq.Editor.checkEditing(); 8 | if (curBlock) { 9 | if (isEditing || (selected && selected.length === 1)) { 10 | const block = await logseq.Editor.getBlock(curBlock.uuid, { 11 | includeChildren: true, 12 | }); 13 | const blocks = block.children; 14 | for (let i = 0; i < blocks.length; i++) { 15 | for (let j = 0; j < blocks.length - i; j++) { 16 | const a = blocks[j] as BlockEntity; 17 | const b = blocks[j + 1] as BlockEntity; 18 | if ( 19 | a && 20 | b && 21 | a.content.localeCompare(b.content, "en", { numeric: true }) > 0 22 | ) { 23 | try { 24 | await logseq.Editor.moveBlock(a.uuid, b.uuid, { 25 | before: false, 26 | children: false, 27 | }); 28 | } catch (e) {} 29 | [blocks[j], blocks[j + 1]] = [blocks[j + 1], blocks[j]]; 30 | } 31 | } 32 | } 33 | } else { 34 | if (selected && selected.length > 1) { 35 | logseq.UI.showMsg("Please select only one block!"); 36 | return; 37 | } 38 | } 39 | } 40 | }; 41 | 42 | const rsortHandler = async () => { 43 | const curBlock = await logseq.Editor.getCurrentBlock(); 44 | const selected = await logseq.Editor.getSelectedBlocks(); 45 | const isEditing = await logseq.Editor.checkEditing(); 46 | if (curBlock) { 47 | if (isEditing || (selected && selected.length === 1)) { 48 | const block = await logseq.Editor.getBlock(curBlock.uuid, { 49 | includeChildren: true, 50 | }); 51 | const blocks = block.children; 52 | for (let i = 0; i < blocks.length; i++) { 53 | for (let j = 0; j < blocks.length - i; j++) { 54 | const a = blocks[j] as BlockEntity; 55 | const b = blocks[j + 1] as BlockEntity; 56 | if ( 57 | a && 58 | b && 59 | a.content.localeCompare(b.content, "en", { numeric: true }) < 0 60 | ) { 61 | try { 62 | await logseq.Editor.moveBlock(a.uuid, b.uuid, { 63 | before: false, 64 | children: false, 65 | }); 66 | } catch (e) {} 67 | [blocks[j], blocks[j + 1]] = [blocks[j + 1], blocks[j]]; 68 | } 69 | } 70 | } 71 | } else { 72 | if (selected && selected.length > 1) { 73 | logseq.UI.showMsg("Please select only one block!"); 74 | return; 75 | } 76 | } 77 | } 78 | }; 79 | 80 | logseq.Editor.registerSlashCommand("Sort Blocks", sortHandler); 81 | logseq.Editor.registerSlashCommand("Reverse Sort Blocks", rsortHandler); 82 | }; 83 | -------------------------------------------------------------------------------- /src/keybindings/prevSibling.ts: -------------------------------------------------------------------------------- 1 | import { BlockUUID, ILSPluginUser } from "@logseq/libs/dist/LSPlugin"; 2 | import { 3 | debug, 4 | getCurrentBlockUUID, 5 | getCurrentPage, 6 | getNumber, 7 | getSettings, 8 | getVisualMode, 9 | resetNumber, 10 | scrollToBlockInPage, 11 | } from "@/common/funcs"; 12 | 13 | const goPrevSibling = async (lastBlockUUID: BlockUUID | undefined) => { 14 | const page = await getCurrentPage(); 15 | if (page?.name) { 16 | let blockUUID = lastBlockUUID || (await getCurrentBlockUUID()); 17 | if (blockUUID) { 18 | let block = await logseq.Editor.getBlock(blockUUID); 19 | if (block?.uuid) { 20 | const prevBlock = await logseq.Editor.getPreviousSiblingBlock( 21 | block.uuid 22 | ); 23 | if (prevBlock?.uuid) { 24 | scrollToBlockInPage(page.name || page.uuid, prevBlock.uuid); 25 | return prevBlock.uuid; 26 | } else if (block.parent.id) { 27 | const parentBlock = await logseq.Editor.getBlock(block.parent.id); 28 | if (parentBlock?.uuid) { 29 | scrollToBlockInPage(page.name || page.uuid, parentBlock.uuid); 30 | return parentBlock.uuid; 31 | } 32 | } 33 | } 34 | } 35 | } else { 36 | let blockUUID = lastBlockUUID || (await getCurrentBlockUUID()); 37 | if (blockUUID) { 38 | let block = await logseq.Editor.getBlock(blockUUID); 39 | if (block?.uuid) { 40 | const prevBlock = await logseq.Editor.getPreviousSiblingBlock( 41 | block.uuid 42 | ); 43 | if (prevBlock?.uuid) { 44 | scrollToBlockInPage(page.uuid, prevBlock.uuid); 45 | return prevBlock.uuid; 46 | } else if (block.parent.id) { 47 | const parentBlock = await logseq.Editor.getBlock(block.parent.id); 48 | if (parentBlock?.uuid) { 49 | scrollToBlockInPage(page.uuid, parentBlock.uuid); 50 | return parentBlock.uuid; 51 | } 52 | } 53 | } 54 | } 55 | } 56 | }; 57 | 58 | export default (logseq: ILSPluginUser) => { 59 | const settings = getSettings(); 60 | 61 | const bindings = Array.isArray(settings.keyBindings.prevSibling) 62 | ? settings.keyBindings.prevSibling 63 | : [settings.keyBindings.prevSibling]; 64 | 65 | bindings.forEach((binding, index) => { 66 | logseq.App.registerCommandPalette( 67 | { 68 | key: "vim-shortcut-prev-sibling-" + index, 69 | label: "Go to previous sibling", 70 | keybinding: { 71 | mode: "non-editing", 72 | binding, 73 | }, 74 | }, 75 | async () => { 76 | const number = getNumber(); 77 | resetNumber(); 78 | 79 | const visualMode = getVisualMode(); 80 | 81 | if (visualMode) { 82 | debug("Move block up"); 83 | for (let i = 0; i < number; i++) { 84 | await logseq.App.invokeExternalCommand( 85 | // @ts-ignore 86 | "logseq.editor/move-block-up" 87 | ); 88 | } 89 | } else { 90 | debug("Prev sibling"); 91 | 92 | let lastBlockUUID: BlockUUID | undefined = undefined; 93 | for (let i = 0; i < number; i++) { 94 | lastBlockUUID = await goPrevSibling(lastBlockUUID); 95 | } 96 | } 97 | } 98 | ); 99 | }); 100 | }; 101 | -------------------------------------------------------------------------------- /src/keybindings/nextSibling.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ILSPluginUser, 3 | BlockEntity, 4 | PageEntity, 5 | BlockUUID, 6 | } from "@logseq/libs/dist/LSPlugin"; 7 | import { 8 | debug, 9 | getCurrentBlockUUID, 10 | getCurrentPage, 11 | getNumber, 12 | getSettings, 13 | getVisualMode, 14 | resetNumber, 15 | scrollToBlockInPage, 16 | } from "@/common/funcs"; 17 | 18 | const findNextBlockRecur = async ( 19 | page: PageEntity | BlockEntity, 20 | block: BlockEntity 21 | ) => { 22 | if (block.parent.id) { 23 | const parentBlock = await logseq.Editor.getBlock(block.parent.id); 24 | if (parentBlock?.uuid) { 25 | const parentNextBlock = await logseq.Editor.getNextSiblingBlock( 26 | parentBlock?.uuid 27 | ); 28 | if (parentNextBlock?.uuid) { 29 | scrollToBlockInPage(page.name || page.uuid, parentNextBlock.uuid); 30 | } else if (parentBlock.parent.id) { 31 | await findNextBlockRecur(page, parentBlock); 32 | } 33 | } 34 | } 35 | }; 36 | 37 | const goNextSibling = async (lastBlockUUID: BlockUUID | undefined) => { 38 | const page = await getCurrentPage(); 39 | 40 | if (page?.name) { 41 | let blockUUID = lastBlockUUID || (await getCurrentBlockUUID()); 42 | if (blockUUID) { 43 | let block = await logseq.Editor.getBlock(blockUUID); 44 | if (block?.uuid) { 45 | const nextBlock = await logseq.Editor.getNextSiblingBlock(block.uuid); 46 | if (nextBlock?.uuid) { 47 | scrollToBlockInPage(page.name || page.uuid, nextBlock.uuid); 48 | return nextBlock?.uuid; 49 | } else if (block.parent.id) { 50 | await findNextBlockRecur(page, block); 51 | } 52 | } 53 | } 54 | } else { 55 | let blockUUID = lastBlockUUID || (await getCurrentBlockUUID()); 56 | if (blockUUID) { 57 | let block = await logseq.Editor.getBlock(blockUUID); 58 | if (block?.uuid) { 59 | const nextBlock = await logseq.Editor.getNextSiblingBlock(block.uuid); 60 | if (nextBlock?.uuid) { 61 | scrollToBlockInPage(page.uuid, nextBlock.uuid); 62 | return nextBlock?.uuid; 63 | } else if (block.parent.id) { 64 | await findNextBlockRecur(page, block); 65 | } 66 | } 67 | } 68 | } 69 | }; 70 | 71 | export default (logseq: ILSPluginUser) => { 72 | const settings = getSettings(); 73 | 74 | const bindings = Array.isArray(settings.keyBindings.nextSibling) 75 | ? settings.keyBindings.nextSibling 76 | : [settings.keyBindings.nextSibling]; 77 | 78 | bindings.forEach((binding, index) => { 79 | logseq.App.registerCommandPalette( 80 | { 81 | key: "vim-shortcut-next-sibling-" + index, 82 | label: "Go to next sibling", 83 | keybinding: { 84 | mode: "non-editing", 85 | binding, 86 | }, 87 | }, 88 | async () => { 89 | const number = getNumber(); 90 | resetNumber(); 91 | 92 | const visualMode = getVisualMode(); 93 | 94 | if (visualMode) { 95 | debug("Move block down"); 96 | for (let i = 0; i < number; i++) { 97 | await logseq.App.invokeExternalCommand( 98 | // @ts-ignore 99 | "logseq.editor/move-block-down" 100 | ); 101 | } 102 | } else { 103 | debug("Next sibling"); 104 | let lastBlockUUID: BlockUUID | undefined; 105 | for (let i = 0; i < number; i++) { 106 | // @ts-ignore 107 | lastBlockUUID = await goNextSibling(lastBlockUUID); 108 | } 109 | } 110 | } 111 | ); 112 | }); 113 | }; 114 | -------------------------------------------------------------------------------- /src/keybindings/search.ts: -------------------------------------------------------------------------------- 1 | import { BlockEntity, ILSPluginUser } from "@logseq/libs/dist/LSPlugin"; 2 | import { 3 | clearCurrentPageBlocksHighlight, 4 | debug, 5 | getSettings, 6 | } from "@/common/funcs"; 7 | import { useSearchStore } from "@/stores/search"; 8 | 9 | export default (logseq: ILSPluginUser) => { 10 | const settings = getSettings(); 11 | 12 | // Search Prev 13 | const searchCleanupBindings = Array.isArray( 14 | settings.keyBindings.searchCleanup 15 | ) 16 | ? settings.keyBindings.searchCleanup 17 | : [settings.keyBindings.searchCleanup]; 18 | 19 | searchCleanupBindings.forEach((binding, index) => { 20 | logseq.App.registerCommandPalette( 21 | { 22 | key: "vim-shortcut-search-cleanup-" + index, 23 | label: "Search Cleanup", 24 | keybinding: { 25 | mode: "global", 26 | binding, 27 | }, 28 | }, 29 | async () => { 30 | debug("Search Cleanup"); 31 | await clearCurrentPageBlocksHighlight(); 32 | // Clear cursor mode as well 33 | const searchStore = useSearchStore(); 34 | searchStore.clearCursor(); 35 | } 36 | ); 37 | }); 38 | 39 | const searchBindings = Array.isArray(settings.keyBindings.search) 40 | ? settings.keyBindings.search 41 | : [settings.keyBindings.search]; 42 | 43 | searchBindings.forEach((binding, index) => { 44 | logseq.App.registerCommandPalette( 45 | { 46 | key: "vim-shortcut-search-" + index, 47 | label: "Search", 48 | keybinding: { 49 | mode: "global", 50 | binding, 51 | }, 52 | }, 53 | async () => { 54 | debug("Search"); 55 | const searchStore = useSearchStore(); 56 | searchStore.emptyInput(); 57 | searchStore.show(); 58 | logseq.showMainUI({ 59 | autoFocus: true, 60 | }); 61 | 62 | await clearCurrentPageBlocksHighlight(); 63 | 64 | const $input = document.querySelector( 65 | ".search-input input" 66 | ) as HTMLInputElement; 67 | setTimeout(() => { 68 | $input && $input.focus(); 69 | }, 500); 70 | } 71 | ); 72 | }); 73 | 74 | // Search Next 75 | const searchNextBindings = Array.isArray(settings.keyBindings.searchNext) 76 | ? settings.keyBindings.searchNext 77 | : [settings.keyBindings.searchNext]; 78 | 79 | searchNextBindings.forEach((binding, index) => { 80 | logseq.App.registerCommandPalette( 81 | { 82 | key: "vim-shortcut-search-next-" + index, 83 | label: "Search Next", 84 | keybinding: { 85 | mode: "global", 86 | binding, 87 | }, 88 | }, 89 | async () => { 90 | debug("Search Next"); 91 | await clearCurrentPageBlocksHighlight(); 92 | const searchStore = useSearchStore(); 93 | searchStore.searchNext(); 94 | } 95 | ); 96 | }); 97 | 98 | // Search Prev 99 | const searchPrevBindings = Array.isArray(settings.keyBindings.searchPrev) 100 | ? settings.keyBindings.searchPrev 101 | : [settings.keyBindings.searchPrev]; 102 | 103 | searchPrevBindings.forEach((binding, index) => { 104 | logseq.App.registerCommandPalette( 105 | { 106 | key: "vim-shortcut-search-prev-" + index, 107 | label: "Search Prev", 108 | keybinding: { 109 | mode: "global", 110 | binding, 111 | }, 112 | }, 113 | async () => { 114 | debug("Search Prev"); 115 | await clearCurrentPageBlocksHighlight(); 116 | const searchStore = useSearchStore(); 117 | searchStore.searchPrev(); 118 | } 119 | ); 120 | }); 121 | }; 122 | -------------------------------------------------------------------------------- /src/keybindings/changeCurrentBlock.ts: -------------------------------------------------------------------------------- 1 | import { BlockEntity, ILSPluginUser } from "@logseq/libs/dist/LSPlugin"; 2 | import { 3 | debug, 4 | getCurrentBlockUUID, 5 | getSettings, 6 | writeClipboard, 7 | } from "@/common/funcs"; 8 | import { useSearchStore } from "@/stores/search"; 9 | 10 | const clearBlockAndEdit = async (currentBlock: BlockEntity): Promise => { 11 | writeClipboard(currentBlock.content); 12 | await logseq.Editor.updateBlock(currentBlock.uuid, ""); 13 | await logseq.Editor.editBlock(currentBlock.uuid); 14 | }; 15 | 16 | const deleteMatchAndEdit = async ( 17 | blockUUID: string, 18 | matchOffset: number, 19 | matchLength: number 20 | ): Promise => { 21 | const block = await logseq.Editor.getBlock(blockUUID); 22 | if (block) { 23 | const matchStart = matchOffset; 24 | const matchEnd = matchStart + matchLength; 25 | 26 | // Remove the matched text from content 27 | const newContent = 28 | block.content.substring(0, matchStart) + 29 | block.content.substring(matchEnd); 30 | 31 | // Update the block with new content 32 | await logseq.Editor.updateBlock(blockUUID, newContent); 33 | 34 | // Small delay to ensure the block is updated 35 | await new Promise(resolve => setTimeout(resolve, 50)); 36 | 37 | // Enter edit mode at the match position 38 | await logseq.Editor.editBlock(blockUUID, { 39 | pos: matchStart, 40 | }); 41 | } 42 | }; 43 | 44 | export default (logseq: ILSPluginUser) => { 45 | const settings = getSettings(); 46 | 47 | const bindings = Array.isArray(settings.keyBindings.changeCurrentBlock) 48 | ? settings.keyBindings.changeCurrentBlock 49 | : [settings.keyBindings.changeCurrentBlock]; 50 | 51 | bindings.forEach((binding, index) => { 52 | logseq.App.registerCommandPalette( 53 | { 54 | key: "vim-shortcut-change-current-block-" + index, 55 | label: "Change current block (or delete match if searching)", 56 | keybinding: { 57 | mode: "non-editing", 58 | binding, 59 | }, 60 | }, 61 | async () => { 62 | debug("change current block"); 63 | 64 | // First check if we're in search mode with an active match 65 | const blockUUID = await getCurrentBlockUUID(); 66 | if (blockUUID) { 67 | const searchStore = useSearchStore(); 68 | const currentMatch = searchStore.getCurrentMatch(); 69 | 70 | // If there's an active search match on this block, delete only the match 71 | if (currentMatch && currentMatch.uuid === blockUUID && searchStore.input) { 72 | await deleteMatchAndEdit(blockUUID, currentMatch.matchOffset, searchStore.input.length); 73 | return; 74 | } 75 | 76 | // If in cursor mode, delete the character at cursor position 77 | if (currentMatch && currentMatch.uuid === blockUUID && searchStore.cursorMode) { 78 | await deleteMatchAndEdit(blockUUID, currentMatch.matchOffset, 1); 79 | searchStore.clearCursor(); 80 | return; 81 | } 82 | } 83 | 84 | // Otherwise, use the original behavior (clear entire block or selection) 85 | const selected = await logseq.Editor.getSelectedBlocks(); 86 | debug(selected) 87 | if (selected && selected.length > 1) { 88 | for (let i = 1; i < selected.length; i++) { 89 | await logseq.Editor.removeBlock(selected[i].uuid); 90 | } 91 | await clearBlockAndEdit(selected[0]); 92 | } else { 93 | // normal mode: clear current block, edit current 94 | if (blockUUID) { 95 | const currentBlock = await logseq.Editor.getBlock(blockUUID); 96 | await clearBlockAndEdit(currentBlock); 97 | } 98 | } 99 | } 100 | ); 101 | }); 102 | }; 103 | -------------------------------------------------------------------------------- /src/keybindings/deleteCurrentBlock.ts: -------------------------------------------------------------------------------- 1 | import { ILSPluginUser } from "@logseq/libs/dist/LSPlugin"; 2 | import { 3 | debug, 4 | getCurrentBlockUUID, 5 | getCurrentPage, 6 | getNumber, 7 | getSettings, 8 | resetNumber, 9 | scrollToBlockInPage, 10 | writeClipboard, 11 | } from "@/common/funcs"; 12 | 13 | const deleteCurrentBlock = async (number: number) => { 14 | const page = await getCurrentPage(); 15 | if (page?.name) { 16 | let blockUUID = await getCurrentBlockUUID(); 17 | if (blockUUID) { 18 | let block = await logseq.Editor.getBlock(blockUUID); 19 | if (block?.uuid) { 20 | let prevBlock = await logseq.Editor.getPreviousSiblingBlock(block.uuid); 21 | let nextBlock, currentBlock; 22 | currentBlock = block; 23 | 24 | for (let i = 0; i < number; i++) { 25 | writeClipboard(currentBlock.content); 26 | nextBlock = await logseq.Editor.getNextSiblingBlock( 27 | currentBlock.uuid 28 | ); 29 | await logseq.Editor.removeBlock(currentBlock.uuid); 30 | if (!nextBlock) { 31 | break; 32 | } else { 33 | currentBlock = nextBlock; 34 | } 35 | } 36 | 37 | let focusBlock = prevBlock || nextBlock || null; 38 | if (focusBlock?.uuid) { 39 | scrollToBlockInPage(page.name, focusBlock.uuid); 40 | } else if (block.left.id) { 41 | const parentBlock = await logseq.Editor.getBlock(block.left.id); 42 | if (parentBlock?.uuid) { 43 | scrollToBlockInPage(page.name, parentBlock.uuid); 44 | } 45 | } 46 | } 47 | } 48 | } else { 49 | let blockUUID = await getCurrentBlockUUID(); 50 | if (blockUUID) { 51 | let block = await logseq.Editor.getBlock(blockUUID); 52 | if (block?.uuid) { 53 | let prevBlock = await logseq.Editor.getPreviousSiblingBlock(block.uuid); 54 | let nextBlock, currentBlock; 55 | currentBlock = block; 56 | 57 | for (let i = 0; i < number; i++) { 58 | writeClipboard(currentBlock.content); 59 | nextBlock = await logseq.Editor.getNextSiblingBlock( 60 | currentBlock.uuid 61 | ); 62 | await logseq.Editor.removeBlock(currentBlock.uuid); 63 | if (!nextBlock) { 64 | break; 65 | } else { 66 | currentBlock = nextBlock; 67 | } 68 | } 69 | 70 | let focusBlock = prevBlock || nextBlock || null; 71 | if (focusBlock?.uuid) { 72 | await logseq.Editor.editBlock(focusBlock.uuid); 73 | await logseq.Editor.exitEditingMode(true); 74 | } else if (block.left.id) { 75 | const parentBlock = await logseq.Editor.getBlock(block.left.id); 76 | if (parentBlock?.uuid) { 77 | await logseq.Editor.editBlock(parentBlock.uuid); 78 | await logseq.Editor.exitEditingMode(true); 79 | } 80 | } 81 | } 82 | } 83 | } 84 | }; 85 | 86 | export default (logseq: ILSPluginUser) => { 87 | const settings = getSettings(); 88 | 89 | const bindings = Array.isArray(settings.keyBindings.deleteCurrentBlock) 90 | ? settings.keyBindings.deleteCurrentBlock 91 | : [settings.keyBindings.deleteCurrentBlock]; 92 | 93 | bindings.forEach((binding, index) => { 94 | logseq.App.registerCommandPalette( 95 | { 96 | key: "vim-shortcut-delete-current-block-" + index, 97 | label: "Delete current block", 98 | keybinding: { 99 | mode: "non-editing", 100 | binding, 101 | }, 102 | }, 103 | async () => { 104 | debug("delete current block"); 105 | 106 | const number = getNumber(); 107 | resetNumber(); 108 | 109 | await deleteCurrentBlock(number); 110 | } 111 | ); 112 | }); 113 | }; 114 | -------------------------------------------------------------------------------- /src/keybindings/deleteCurrentAndNextSiblingBlocks.ts: -------------------------------------------------------------------------------- 1 | import { ILSPluginUser } from "@logseq/libs/dist/LSPlugin"; 2 | import { 3 | debug, 4 | getCurrentBlockUUID, 5 | getCurrentPage, 6 | getNumber, 7 | getSettings, 8 | resetNumber, 9 | scrollToBlockInPage, 10 | writeClipboard, 11 | } from "@/common/funcs"; 12 | 13 | const deleteCurrentAndNextSiblingBlocks = async (number: number) => { 14 | const page = await getCurrentPage(); 15 | if (page?.name) { 16 | let blockUUID = await getCurrentBlockUUID(); 17 | if (blockUUID) { 18 | let block = await logseq.Editor.getBlock(blockUUID); 19 | if (block?.uuid) { 20 | let prevBlock = await logseq.Editor.getPreviousSiblingBlock(block.uuid); 21 | let nextBlock, currentBlock; 22 | currentBlock = block; 23 | 24 | for (let i = 0; i <= number; i++) { 25 | writeClipboard(currentBlock.content); 26 | nextBlock = await logseq.Editor.getNextSiblingBlock( 27 | currentBlock.uuid 28 | ); 29 | await logseq.Editor.removeBlock(currentBlock.uuid); 30 | if (!nextBlock) { 31 | break; 32 | } else { 33 | currentBlock = nextBlock; 34 | } 35 | } 36 | 37 | let focusBlock = prevBlock || nextBlock || null; 38 | if (focusBlock?.uuid) { 39 | scrollToBlockInPage(page.name, focusBlock.uuid); 40 | } else if (block.left.id) { 41 | const parentBlock = await logseq.Editor.getBlock(block.left.id); 42 | if (parentBlock?.uuid) { 43 | scrollToBlockInPage(page.name, parentBlock.uuid); 44 | } 45 | } 46 | } 47 | } 48 | } else { 49 | let blockUUID = await getCurrentBlockUUID(); 50 | if (blockUUID) { 51 | let block = await logseq.Editor.getBlock(blockUUID); 52 | if (block?.uuid) { 53 | let prevBlock = await logseq.Editor.getPreviousSiblingBlock(block.uuid); 54 | let nextBlock, currentBlock; 55 | currentBlock = block; 56 | 57 | for (let i = 0; i <= number; i++) { 58 | writeClipboard(currentBlock.content); 59 | nextBlock = await logseq.Editor.getNextSiblingBlock( 60 | currentBlock.uuid 61 | ); 62 | await logseq.Editor.removeBlock(currentBlock.uuid); 63 | if (!nextBlock) { 64 | break; 65 | } else { 66 | currentBlock = nextBlock; 67 | } 68 | } 69 | 70 | let focusBlock = prevBlock || nextBlock || null; 71 | if (focusBlock?.uuid) { 72 | await logseq.Editor.editBlock(focusBlock.uuid); 73 | await logseq.Editor.exitEditingMode(true); 74 | } else if (block.left.id) { 75 | const parentBlock = await logseq.Editor.getBlock(block.left.id); 76 | if (parentBlock?.uuid) { 77 | await logseq.Editor.editBlock(parentBlock.uuid); 78 | await logseq.Editor.exitEditingMode(true); 79 | } 80 | } 81 | } 82 | } 83 | } 84 | }; 85 | 86 | export default (logseq: ILSPluginUser) => { 87 | const settings = getSettings(); 88 | 89 | const bindings = Array.isArray( 90 | settings.keyBindings.deleteCurrentAndNextSiblingBlocks 91 | ) 92 | ? settings.keyBindings.deleteCurrentAndNextSiblingBlocks 93 | : [settings.keyBindings.deleteCurrentAndNextSiblingBlocks]; 94 | 95 | bindings.forEach((binding, index) => { 96 | logseq.App.registerCommandPalette( 97 | { 98 | key: "vim-shortcut-delete-current-and-next-blocks-" + index, 99 | label: "Delete current and next blocks", 100 | keybinding: { 101 | mode: "non-editing", 102 | binding, 103 | }, 104 | }, 105 | async () => { 106 | debug("delete current and next blocks"); 107 | 108 | const number = getNumber(); 109 | resetNumber(); 110 | 111 | await deleteCurrentAndNextSiblingBlocks(number); 112 | } 113 | ); 114 | }); 115 | }; 116 | -------------------------------------------------------------------------------- /src/keybindings/deleteCurrentAndPrevSiblingBlocks.ts: -------------------------------------------------------------------------------- 1 | import { ILSPluginUser } from "@logseq/libs/dist/LSPlugin"; 2 | import { 3 | debug, 4 | getCurrentBlockUUID, 5 | getCurrentPage, 6 | getNumber, 7 | getSettings, 8 | resetNumber, 9 | scrollToBlockInPage, 10 | writeClipboard, 11 | } from "@/common/funcs"; 12 | 13 | const deleteCurrentAndPrevSiblingBlocks = async (number: number) => { 14 | const page = await getCurrentPage(); 15 | if (page?.name) { 16 | let blockUUID = await getCurrentBlockUUID(); 17 | if (blockUUID) { 18 | let block = await logseq.Editor.getBlock(blockUUID); 19 | if (block?.uuid) { 20 | let nextBlock = await logseq.Editor.getNextSiblingBlock(block.uuid); 21 | let prevBlock, currentBlock; 22 | currentBlock = block; 23 | 24 | for (let i = 0; i <= number; i++) { 25 | writeClipboard(currentBlock.content); 26 | prevBlock = await logseq.Editor.getPreviousSiblingBlock( 27 | currentBlock.uuid 28 | ); 29 | await logseq.Editor.removeBlock(currentBlock.uuid); 30 | if (!prevBlock) { 31 | break; 32 | } else { 33 | currentBlock = prevBlock; 34 | } 35 | } 36 | 37 | let focusBlock = nextBlock || prevBlock || null; 38 | if (focusBlock?.uuid) { 39 | scrollToBlockInPage(page.name, focusBlock.uuid); 40 | } else if (block.left.id) { 41 | const parentBlock = await logseq.Editor.getBlock(block.left.id); 42 | if (parentBlock?.uuid) { 43 | scrollToBlockInPage(page.name, parentBlock.uuid); 44 | } 45 | } 46 | } 47 | } 48 | } else { 49 | let blockUUID = await getCurrentBlockUUID(); 50 | if (blockUUID) { 51 | let block = await logseq.Editor.getBlock(blockUUID); 52 | if (block?.uuid) { 53 | let nextBlock = await logseq.Editor.getPreviousSiblingBlock(block.uuid); 54 | let prevBlock, currentBlock; 55 | currentBlock = block; 56 | 57 | for (let i = 0; i <= number; i++) { 58 | writeClipboard(currentBlock.content); 59 | prevBlock = await logseq.Editor.getNextSiblingBlock( 60 | currentBlock.uuid 61 | ); 62 | await logseq.Editor.removeBlock(currentBlock.uuid); 63 | if (!prevBlock) { 64 | break; 65 | } else { 66 | currentBlock = prevBlock; 67 | } 68 | } 69 | 70 | let focusBlock = nextBlock || prevBlock || null; 71 | if (focusBlock?.uuid) { 72 | await logseq.Editor.editBlock(focusBlock.uuid); 73 | await logseq.Editor.exitEditingMode(true); 74 | } else if (block.left.id) { 75 | const parentBlock = await logseq.Editor.getBlock(block.left.id); 76 | if (parentBlock?.uuid) { 77 | await logseq.Editor.editBlock(parentBlock.uuid); 78 | await logseq.Editor.exitEditingMode(true); 79 | } 80 | } 81 | } 82 | } 83 | } 84 | }; 85 | 86 | export default (logseq: ILSPluginUser) => { 87 | const settings = getSettings(); 88 | 89 | const bindings = Array.isArray( 90 | settings.keyBindings.deleteCurrentAndPrevSiblingBlocks 91 | ) 92 | ? settings.keyBindings.deleteCurrentAndPrevSiblingBlocks 93 | : [settings.keyBindings.deleteCurrentAndPrevSiblingBlocks]; 94 | 95 | bindings.forEach((binding, index) => { 96 | logseq.App.registerCommandPalette( 97 | { 98 | key: "vim-shortcut-delete-current-and-prev-blocks-" + index, 99 | label: "Delete current and prev blocks", 100 | keybinding: { 101 | mode: "non-editing", 102 | binding, 103 | }, 104 | }, 105 | async () => { 106 | debug("delete current and prev blocks"); 107 | 108 | const number = getNumber(); 109 | resetNumber(); 110 | 111 | await deleteCurrentAndPrevSiblingBlocks(number); 112 | } 113 | ); 114 | }); 115 | }; 116 | -------------------------------------------------------------------------------- /src/stores/command.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from "pinia"; 2 | export const commandList = [ 3 | { 4 | value: "s/", 5 | desc: "Replace current block according regex.", 6 | wait: true, 7 | }, 8 | { 9 | value: "substitute/", 10 | desc: "Replace current block according regex.", 11 | wait: true, 12 | }, 13 | { 14 | value: "%s/", 15 | desc: "Replace current page blocks according regex.", 16 | wait: true, 17 | }, 18 | { 19 | value: "%substitute/", 20 | desc: "Replace current page blocks according regex.", 21 | wait: true, 22 | }, 23 | { value: "m", desc: "Go to marked page or block.", wait: true }, 24 | { value: "mark", desc: "Go to marked page or block.", wait: true }, 25 | { value: "marks", desc: "Show marks." }, 26 | { value: "delm", desc: "Delete specific marks.", wait: true }, 27 | { value: "delmarks", desc: "Delete specific marks.", wait: true }, 28 | { value: "delm!", desc: "Delete all marks.", wait: false }, 29 | { value: "delmarks!", desc: "Delete all marks.", wait: false }, 30 | { value: "w", desc: "Save current page." }, 31 | { value: "write", desc: "Save current page." }, 32 | { value: "wq", desc: "Save current page and quit vim command mode." }, 33 | { value: "q", desc: "Quit vim command mode." }, 34 | { value: "quit", desc: "Quit vim command mode." }, 35 | { value: "re", desc: "Rename current page.", wait: true }, 36 | { value: "rename", desc: "Rename current page.", wait: true }, 37 | { value: "undo", desc: "Undo last edit.", wait: false }, 38 | { value: "redo", desc: "Redo last edit.", wait: false }, 39 | { value: "lorem", desc: "Random generate blocks.", wait: true }, 40 | { 41 | value: "emoji-picker", 42 | desc: "Search and input emojis using emoji picker.", 43 | wait: false, 44 | }, 45 | { value: "emoji", desc: "Search and input emojis.", wait: true }, 46 | { value: "sort", desc: "Sort blocks a-z.", wait: false }, 47 | { value: "rsort", desc: "Sort blocks z-a.", wait: false }, 48 | { 49 | value: "bg-picker", 50 | desc: "Set block backgroud color using color picker.", 51 | wait: false, 52 | }, 53 | { value: "bg", desc: "Set block backgroud color.", wait: true }, 54 | { value: "bg-clear", desc: "Clear block backgroud color.", wait: false }, 55 | { 56 | value: "bg-random", 57 | desc: "Set block backgroud color randomly.", 58 | wait: false, 59 | }, 60 | { 61 | value: "copy-path", 62 | desc: "Copy page or journal path for editing it in other editor.", 63 | }, 64 | { 65 | value: "open-in-vscode", 66 | desc: "Open page in VSCode.", 67 | }, 68 | { 69 | value: "go ", 70 | desc: "go to existed page.", 71 | wait: true, 72 | }, 73 | { 74 | value: "go! ", 75 | desc: "Create new page or go to existed page.", 76 | wait: true, 77 | }, 78 | { 79 | value: "clear-highlights", 80 | desc: "Clear all blocks highlight on current page.", 81 | wait: false, 82 | }, 83 | { 84 | value: "h", 85 | desc: "Show help modal.", 86 | wait: false, 87 | }, 88 | { 89 | value: "help", 90 | desc: "Show help modal.", 91 | wait: false, 92 | }, 93 | ]; 94 | export const useCommandStore = defineStore("command", { 95 | state: () => ({ 96 | commandList, 97 | triggerOnFocus: false, 98 | input: "", 99 | visible: false, 100 | }), 101 | actions: { 102 | show() { 103 | this.input = ""; 104 | this.visible = true; 105 | }, 106 | hide() { 107 | this.visible = false; 108 | }, 109 | setVisible(visible) { 110 | this.visible = visible; 111 | }, 112 | getCommandList() { 113 | return this.commandList; 114 | }, 115 | enableTriggerOnFocus() { 116 | this.triggerOnFocus = true; 117 | }, 118 | disableTriggerOnFocus() { 119 | this.triggerOnFocus = true; 120 | }, 121 | emptyInput() { 122 | this.input = ""; 123 | }, 124 | setInput(input) { 125 | this.input = input; 126 | }, 127 | }, 128 | }); 129 | -------------------------------------------------------------------------------- /src/keybindings/mark.ts: -------------------------------------------------------------------------------- 1 | import { ILSPluginUser } from "@logseq/libs/dist/LSPlugin"; 2 | import { 3 | debug, 4 | getCurrentPage, 5 | getMark, 6 | getNumber, 7 | getSettings, 8 | resetNumber, 9 | setMark, 10 | } from "@/common/funcs"; 11 | 12 | export default (logseq: ILSPluginUser) => { 13 | const settings = getSettings(); 14 | 15 | const bindingsMarkSave = Array.isArray(settings.keyBindings.markSave) 16 | ? settings.keyBindings.markSave 17 | : [settings.keyBindings.markSave]; 18 | const bindingsMarkJump = Array.isArray(settings.keyBindings.markJump) 19 | ? settings.keyBindings.markJump 20 | : [settings.keyBindings.markJump]; 21 | const bindingsMarkJumpSidebar = Array.isArray( 22 | settings.keyBindings.markJumpSidebar 23 | ) 24 | ? settings.keyBindings.markJumpSidebar 25 | : [settings.keyBindings.markJumpSidebar]; 26 | 27 | bindingsMarkSave.forEach((binding, index) => { 28 | logseq.App.registerCommandPalette( 29 | { 30 | key: "vim-shortcut-save-mark-" + index, 31 | label: "Save mark", 32 | keybinding: { 33 | mode: "non-editing", 34 | binding, 35 | }, 36 | }, 37 | async () => { 38 | debug("Save mark"); 39 | 40 | const number = getNumber(); 41 | resetNumber(); 42 | 43 | const page = await getCurrentPage(); 44 | if (page?.name) { 45 | const block = await logseq.Editor.getCurrentBlock(); 46 | const selected = await logseq.Editor.getSelectedBlocks(); 47 | 48 | // 1. current block uuid exist 49 | // 2. current block page id === current page id 50 | // 3. current block is selected and only current block is selected 51 | if ( 52 | block?.uuid && 53 | block.page.id === page.id && 54 | selected && 55 | selected.length === 1 && 56 | selected[0].uuid === block.uuid 57 | ) { 58 | if (!block?.properties?.id) { 59 | await logseq.Editor.upsertBlockProperty( 60 | block.uuid, 61 | "id", 62 | block.uuid 63 | ); 64 | } 65 | await setMark(number, page.name, block.uuid); 66 | logseq.UI.showMsg(`Mark ${number} saved`); 67 | } else { 68 | await setMark(number, page.name); 69 | logseq.UI.showMsg(`Mark ${number} saved`); 70 | } 71 | } 72 | } 73 | ); 74 | }); 75 | 76 | bindingsMarkJump.forEach((binding, index) => { 77 | logseq.App.registerCommandPalette( 78 | { 79 | key: "vim-shortcut-jump-mark-" + index, 80 | label: "Jump mark", 81 | keybinding: { 82 | mode: "non-editing", 83 | binding, 84 | }, 85 | }, 86 | async () => { 87 | debug("Jump mark"); 88 | 89 | const number = getNumber(); 90 | resetNumber(); 91 | 92 | const mark = getMark(number); 93 | 94 | if (mark) { 95 | if (mark.block) { 96 | logseq.Editor.scrollToBlockInPage(mark.page, mark.block); 97 | } else { 98 | logseq.App.pushState("page", { 99 | name: mark.page, 100 | }); 101 | } 102 | } 103 | } 104 | ); 105 | }); 106 | 107 | bindingsMarkJumpSidebar.forEach((binding, index) => { 108 | logseq.App.registerCommandPalette( 109 | { 110 | key: "vim-shortcut-jump-mark-sidebar-" + index, 111 | label: "Jump mark to sidebar", 112 | keybinding: { 113 | mode: "non-editing", 114 | binding, 115 | }, 116 | }, 117 | async () => { 118 | debug("Jump mark to sidebar"); 119 | 120 | const number = getNumber(); 121 | resetNumber(); 122 | 123 | const mark = getMark(number); 124 | 125 | if (mark) { 126 | if (mark.block) { 127 | logseq.App.openInRightSidebar(mark.block); 128 | } else { 129 | const page = await logseq.Editor.getPage(mark.page); 130 | if (page?.uuid) { 131 | logseq.App.openInRightSidebar(page.uuid); 132 | } 133 | } 134 | } 135 | } 136 | ); 137 | }); 138 | }; 139 | -------------------------------------------------------------------------------- /src/commands/sort.ts: -------------------------------------------------------------------------------- 1 | import "@logseq/libs"; 2 | import { BlockEntity } from "@logseq/libs/dist/LSPlugin"; 3 | 4 | export async function sort() { 5 | const curBlock = await logseq.Editor.getCurrentBlock(); 6 | const selected = await logseq.Editor.getSelectedBlocks(); 7 | const isEditing = await logseq.Editor.checkEditing(); 8 | let pageMode = false; 9 | if (curBlock) { 10 | if (isEditing || (selected && selected.length === 1)) { 11 | const block = await logseq.Editor.getBlock(curBlock.uuid, { 12 | includeChildren: true, 13 | }); 14 | const blocks = block.children; 15 | for (let i = 0; i < blocks.length; i++) { 16 | for (let j = 0; j < blocks.length - i; j++) { 17 | const a = blocks[j] as BlockEntity; 18 | const b = blocks[j + 1] as BlockEntity; 19 | if ( 20 | a && 21 | b && 22 | a.content.localeCompare(b.content, "en", { numeric: true }) > 0 23 | ) { 24 | try { 25 | await logseq.Editor.moveBlock(a.uuid, b.uuid, { 26 | before: false, 27 | children: false, 28 | }); 29 | } catch (e) {} 30 | [blocks[j], blocks[j + 1]] = [blocks[j + 1], blocks[j]]; 31 | } 32 | } 33 | } 34 | return; 35 | } else { 36 | if (selected && selected.length > 1) { 37 | logseq.UI.showMsg("Please select only one block!"); 38 | return; 39 | } 40 | } 41 | pageMode = true; 42 | } else { 43 | pageMode = true; 44 | } 45 | 46 | if (pageMode) { 47 | const page = await logseq.Editor.getCurrentPage(); 48 | if (!page) { 49 | logseq.UI.showMsg("No page selected"); 50 | return; 51 | } 52 | 53 | const blocks = await logseq.Editor.getCurrentPageBlocksTree(); 54 | 55 | for (let i = 0; i < blocks.length; i++) { 56 | for (let j = 0; j < blocks.length - i; j++) { 57 | const a = blocks[j]; 58 | const b = blocks[j + 1]; 59 | if ( 60 | a && 61 | b && 62 | a.content.localeCompare(b.content, "en", { numeric: true }) > 0 63 | ) { 64 | try { 65 | await logseq.Editor.moveBlock(a.uuid, b.uuid, { 66 | before: false, 67 | children: false, 68 | }); 69 | } catch (e) {} 70 | [blocks[j], blocks[j + 1]] = [blocks[j + 1], blocks[j]]; 71 | } 72 | } 73 | } 74 | } 75 | } 76 | 77 | export async function rsort() { 78 | const curBlock = await logseq.Editor.getCurrentBlock(); 79 | const selected = await logseq.Editor.getSelectedBlocks(); 80 | const isEditing = await logseq.Editor.checkEditing(); 81 | let pageMode = false; 82 | if (curBlock) { 83 | if (isEditing || (selected && selected.length === 1)) { 84 | const block = await logseq.Editor.getBlock(curBlock.uuid, { 85 | includeChildren: true, 86 | }); 87 | const blocks = block.children; 88 | for (let i = 0; i < blocks.length; i++) { 89 | for (let j = 0; j < blocks.length - i; j++) { 90 | const a = blocks[j] as BlockEntity; 91 | const b = blocks[j + 1] as BlockEntity; 92 | if ( 93 | a && 94 | b && 95 | a.content.localeCompare(b.content, "en", { numeric: true }) < 0 96 | ) { 97 | try { 98 | await logseq.Editor.moveBlock(a.uuid, b.uuid, { 99 | before: false, 100 | children: false, 101 | }); 102 | } catch (e) {} 103 | [blocks[j], blocks[j + 1]] = [blocks[j + 1], blocks[j]]; 104 | } 105 | } 106 | } 107 | return; 108 | } else { 109 | if (selected && selected.length > 1) { 110 | logseq.UI.showMsg("Please select only one block!"); 111 | return; 112 | } 113 | } 114 | pageMode = true; 115 | } else { 116 | pageMode = true; 117 | } 118 | 119 | if (pageMode) { 120 | const page = await logseq.Editor.getCurrentPage(); 121 | if (!page) { 122 | logseq.UI.showMsg("No page selected"); 123 | return; 124 | } 125 | 126 | const blocks = await logseq.Editor.getCurrentPageBlocksTree(); 127 | 128 | for (let i = 0; i < blocks.length; i++) { 129 | for (let j = 0; j < blocks.length - i; j++) { 130 | const a = blocks[j]; 131 | const b = blocks[j + 1]; 132 | if ( 133 | a && 134 | b && 135 | a.content.localeCompare(b.content, "en", { numeric: true }) < 0 136 | ) { 137 | try { 138 | await logseq.Editor.moveBlock(a.uuid, b.uuid, { 139 | before: false, 140 | children: false, 141 | }); 142 | } catch (e) {} 143 | [blocks[j], blocks[j + 1]] = [blocks[j + 1], blocks[j]]; 144 | } 145 | } 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/keybindings/changeCase.ts: -------------------------------------------------------------------------------- 1 | import { ILSPluginUser } from "@logseq/libs/dist/LSPlugin"; 2 | import * as cc from "change-case-all"; 3 | import { debug, getNumber, getSettings, resetNumber } from "@/common/funcs"; 4 | 5 | export default (logseq: ILSPluginUser) => { 6 | const settings = getSettings(); 7 | 8 | const bindings = Array.isArray(settings.keyBindings.changeCase) 9 | ? settings.keyBindings.changeCase 10 | : [settings.keyBindings.changeCase]; 11 | 12 | bindings.forEach((binding, index) => { 13 | logseq.App.registerCommandPalette( 14 | { 15 | key: "vim-shortcut-change-case-" + index, 16 | label: "Change case", 17 | keybinding: { 18 | mode: "global", 19 | binding, 20 | }, 21 | }, 22 | async () => { 23 | debug("Change case"); 24 | 25 | const number = getNumber(); 26 | resetNumber(); 27 | // const actions = { 28 | // 1: 'upperCaseToggle', 29 | // 2: 'upperCase', 30 | // 3: 'lowerCase', 31 | // 4: 'titleCase', 32 | // 5: 'sentenceCase', 33 | // 6: 'pathCase', 34 | // 7: 'capitalCase', 35 | // 8: 'constantCase', 36 | // 9: 'dotCase', 37 | // 10: 'headerCase', 38 | // 11: 'paramCase', 39 | // 12: 'pascalCase', 40 | // 13: 'camelCase', 41 | // 14: 'snakeCase', 42 | // 15: 'swapCase', 43 | // 16: 'spongeCase' 44 | // }; 45 | 46 | const block = await logseq.Editor.getCurrentBlock(); 47 | if (block && block.content) { 48 | const content = block.content; 49 | 50 | switch (number) { 51 | case 1: 52 | if (cc.isUpperCase(content)) { 53 | await logseq.Editor.updateBlock( 54 | block.uuid, 55 | cc.lowerCase(content) 56 | ); 57 | } else { 58 | await logseq.Editor.updateBlock( 59 | block.uuid, 60 | cc.upperCase(content) 61 | ); 62 | } 63 | break; 64 | case 2: 65 | await logseq.Editor.updateBlock( 66 | block.uuid, 67 | cc.upperCase(content) 68 | ); 69 | break; 70 | case 3: 71 | await logseq.Editor.updateBlock( 72 | block.uuid, 73 | cc.lowerCase(content) 74 | ); 75 | break; 76 | case 4: 77 | await logseq.Editor.updateBlock( 78 | block.uuid, 79 | cc.titleCase(content) 80 | ); 81 | break; 82 | case 5: 83 | await logseq.Editor.updateBlock( 84 | block.uuid, 85 | cc.sentenceCase(content) 86 | ); 87 | break; 88 | case 6: 89 | await logseq.Editor.updateBlock(block.uuid, cc.pathCase(content)); 90 | break; 91 | case 7: 92 | await logseq.Editor.updateBlock( 93 | block.uuid, 94 | cc.capitalCase(content) 95 | ); 96 | break; 97 | case 8: 98 | await logseq.Editor.updateBlock( 99 | block.uuid, 100 | cc.constantCase(content) 101 | ); 102 | break; 103 | case 9: 104 | await logseq.Editor.updateBlock(block.uuid, cc.dotCase(content)); 105 | break; 106 | case 10: 107 | await logseq.Editor.updateBlock( 108 | block.uuid, 109 | cc.headerCase(content) 110 | ); 111 | break; 112 | case 11: 113 | await logseq.Editor.updateBlock( 114 | block.uuid, 115 | cc.paramCase(content) 116 | ); 117 | break; 118 | case 12: 119 | await logseq.Editor.updateBlock( 120 | block.uuid, 121 | cc.pascalCase(content) 122 | ); 123 | break; 124 | case 13: 125 | await logseq.Editor.updateBlock( 126 | block.uuid, 127 | cc.camelCase(content) 128 | ); 129 | break; 130 | case 14: 131 | await logseq.Editor.updateBlock( 132 | block.uuid, 133 | cc.snakeCase(content) 134 | ); 135 | break; 136 | case 15: 137 | await logseq.Editor.updateBlock(block.uuid, cc.swapCase(content)); 138 | break; 139 | case 16: 140 | await logseq.Editor.updateBlock( 141 | block.uuid, 142 | cc.spongeCase(content) 143 | ); 144 | break; 145 | default: 146 | break; 147 | } 148 | } 149 | } 150 | ); 151 | }); 152 | }; 153 | -------------------------------------------------------------------------------- /src/commands/go.ts: -------------------------------------------------------------------------------- 1 | import { hideMainUI } from "@/common/funcs"; 2 | import "@logseq/libs"; 3 | import { format, add, sub } from "date-fns"; 4 | 5 | import { backward, forward } from "./invoke"; 6 | 7 | const parsePageName = async (pageName: string) => { 8 | const config = await logseq.App.getUserConfigs(); 9 | const page = await logseq.Editor.getCurrentPage(); 10 | switch (pageName) { 11 | case "@": 12 | case "@index": 13 | return "Contents"; 14 | case "@today": 15 | pageName = format(new Date(), config.preferredDateFormat); 16 | return pageName; 17 | case "@yesterday": 18 | pageName = format( 19 | sub(new Date(), { 20 | days: 1, 21 | }), 22 | config.preferredDateFormat 23 | ); 24 | return pageName; 25 | case "@tomorrow": 26 | pageName = format( 27 | add(new Date(), { 28 | days: 1, 29 | }), 30 | config.preferredDateFormat 31 | ); 32 | return pageName; 33 | case "@prev": 34 | if (page && page["journal?"]) { 35 | pageName = format( 36 | sub(new Date(page.name), { 37 | days: 1, 38 | }), 39 | config.preferredDateFormat 40 | ); 41 | return pageName; 42 | } else { 43 | pageName = format( 44 | sub(new Date(), { 45 | days: 1, 46 | }), 47 | config.preferredDateFormat 48 | ); 49 | return pageName; 50 | } 51 | case "@next": 52 | if (page && page["journal?"]) { 53 | pageName = format( 54 | add(new Date(page.name), { 55 | days: 1, 56 | }), 57 | config.preferredDateFormat 58 | ); 59 | return pageName; 60 | } else { 61 | pageName = format( 62 | add(new Date(), { 63 | days: 1, 64 | }), 65 | config.preferredDateFormat 66 | ); 67 | return pageName; 68 | } 69 | case "@back": 70 | await backward(); 71 | return; 72 | case "@forward": 73 | await forward(); 74 | return; 75 | default: 76 | return pageName; 77 | } 78 | }; 79 | 80 | export async function goOrCreate(pageName, opts) { 81 | const isBlock = /\(\((.*?)\)\)/.test(pageName); 82 | 83 | if (!isBlock) { 84 | if (opts.ns || opts.namespace) { 85 | let currentPage = await logseq.Editor.getCurrentPage(); 86 | if (!currentPage) { 87 | let block = await logseq.Editor.getCurrentBlock(); 88 | if (block) { 89 | currentPage = await logseq.Editor.getPage(block.page.id); 90 | } 91 | } 92 | if (currentPage) { 93 | pageName = `${currentPage.name}/${pageName}`; 94 | } 95 | } 96 | pageName = await parsePageName(pageName); 97 | if (pageName) { 98 | let page = await logseq.Editor.getPage(pageName); 99 | 100 | if (!page) { 101 | page = await logseq.Editor.createPage( 102 | pageName, 103 | {}, 104 | { 105 | createFirstBlock: true, 106 | redirect: true, 107 | } 108 | ); 109 | const blocks = await logseq.Editor.getPageBlocksTree(pageName); 110 | await logseq.Editor.editBlock(blocks[0].uuid); 111 | } else { 112 | const blocks = await logseq.Editor.getPageBlocksTree(pageName); 113 | logseq.App.pushState("page", { 114 | name: pageName, 115 | }); 116 | if (blocks && blocks.length > 0) { 117 | logseq.Editor.editBlock(blocks[0].uuid); 118 | } 119 | } 120 | } 121 | } else { 122 | const match = pageName.match(/\(\((.*?)\)\)/); 123 | const blockId = match[1]; 124 | const block = await logseq.Editor.getBlock(blockId); 125 | if (block) { 126 | const page = await logseq.Editor.getPage(block.page.id); 127 | await logseq.Editor.scrollToBlockInPage(page.name, blockId); 128 | } else { 129 | logseq.UI.showMsg("Block not exist!"); 130 | } 131 | } 132 | hideMainUI(); 133 | } 134 | 135 | export async function go(pageName, opts) { 136 | const isBlock = /\(\((.*?)\)\)/.test(pageName); 137 | 138 | if (!isBlock) { 139 | if (opts.ns || opts.namespace) { 140 | let currentPage = await logseq.Editor.getCurrentPage(); 141 | if (!currentPage) { 142 | let block = await logseq.Editor.getCurrentBlock(); 143 | if (block) { 144 | currentPage = await logseq.Editor.getPage(block.page.id); 145 | } 146 | } 147 | if (currentPage) { 148 | pageName = `${currentPage.name}/${pageName}`; 149 | } 150 | } 151 | pageName = await parsePageName(pageName); 152 | if (pageName) { 153 | let page = await logseq.Editor.getPage(pageName); 154 | 155 | if (!page) { 156 | logseq.UI.showMsg( 157 | "Page not exist! If you want create non-exist page, use go! command." 158 | ); 159 | } else { 160 | const blocks = await logseq.Editor.getPageBlocksTree(pageName); 161 | logseq.App.pushState("page", { 162 | name: pageName, 163 | }); 164 | if (blocks && blocks.length > 0) { 165 | logseq.Editor.editBlock(blocks[0].uuid); 166 | } 167 | } 168 | } 169 | } else { 170 | const match = pageName.match(/\(\((.*?)\)\)/); 171 | const blockId = match[1]; 172 | const block = await logseq.Editor.getBlock(blockId); 173 | if (block) { 174 | const page = await logseq.Editor.getPage(block.page.id); 175 | await logseq.Editor.scrollToBlockInPage(page.name, blockId); 176 | } else { 177 | logseq.UI.showMsg("Block not exist!"); 178 | } 179 | } 180 | hideMainUI(); 181 | } 182 | -------------------------------------------------------------------------------- /src/commands/page.ts: -------------------------------------------------------------------------------- 1 | import { clearCurrentPageBlocksHighlight, hideMainUI } from "@/common/funcs"; 2 | import "@logseq/libs"; 3 | import { useCopyTextStore } from "@/stores/copy-text"; 4 | import { format } from "date-fns"; 5 | 6 | const replaceBlock = async (block, regex, replace) => { 7 | const replaced = block.content.replace(regex, replace); 8 | await logseq.Editor.updateBlock(block.uuid, replaced); 9 | }; 10 | 11 | function parseEscapeSequences(str) { 12 | return str 13 | .replace(/\\n/g, "\n") 14 | .replace(/\\t/g, "\t") 15 | .replace(/\\r/g, "\r") 16 | .replace(/\\s/g, " "); 17 | // 可以根据需要支持其他转义字符,比如 \\r, \\s 等 18 | } 19 | 20 | const walkReplace = async (blocks: any[], regex, replace) => { 21 | if (blocks && blocks.length > 0) { 22 | for (let block of blocks) { 23 | const { children } = block; 24 | if (children && children.length > 0) { 25 | await walkReplace(children, regex, replace); 26 | } 27 | 28 | await replaceBlock(block, regex, replace); 29 | } 30 | } 31 | }; 32 | 33 | export async function openInVSCode() { 34 | let page = await logseq.Editor.getCurrentPage(); 35 | if (!page) { 36 | let block = await logseq.Editor.getCurrentBlock(); 37 | if (block?.page.id) { 38 | page = await logseq.Editor.getPage(block.page.id); 39 | } 40 | } 41 | const graph = await logseq.App.getCurrentGraph(); 42 | let pagePath; 43 | if (page && graph) { 44 | const { path } = graph; 45 | if (page["journal?"]) { 46 | const fileName = [ 47 | `${page.journalDay}`.substring(0, 4), 48 | `${page.journalDay}`.substring(4, 6), 49 | `${page.journalDay}`.substring(6), 50 | ].join("_"); 51 | pagePath = `${path}/journals/${fileName}.md`; 52 | } else { 53 | const fileName = page.originalName 54 | .replace(/^\//, "") 55 | .replace(/\/$/, "") 56 | .replace(/\//g, ".") 57 | .replace(/[:*?"<>|]+/g, "_") 58 | .replace(/[\\#|%]+/g, "_"); 59 | pagePath = `${path}/pages/${fileName}.md`; 60 | } 61 | 62 | window.open(`vscode://file/${pagePath}`, "_blank"); 63 | } 64 | } 65 | 66 | export async function copyPath() { 67 | const copyTextStore = useCopyTextStore(); 68 | let page = await logseq.Editor.getCurrentPage(); 69 | if (!page) { 70 | let block = await logseq.Editor.getCurrentBlock(); 71 | if (block?.page.id) { 72 | page = await logseq.Editor.getPage(block.page.id); 73 | } 74 | } 75 | const graph = await logseq.App.getCurrentGraph(); 76 | let pagePath; 77 | if (page && graph) { 78 | const { path } = graph; 79 | if (page["journal?"]) { 80 | const fileName = [ 81 | `${page.journalDay}`.substring(0, 4), 82 | `${page.journalDay}`.substring(4, 6), 83 | `${page.journalDay}`.substring(6), 84 | ].join("_"); 85 | pagePath = `${path}/journals/${fileName}.md`; 86 | } else { 87 | const fileName = page.originalName 88 | .replace(/^\//, "") 89 | .replace(/\/$/, "") 90 | .replace(/\//g, ".") 91 | .replace(/[:*?"<>|]+/g, "_") 92 | .replace(/[\\#|%]+/g, "_"); 93 | pagePath = `${path}/pages/${fileName}.md`; 94 | } 95 | 96 | copyTextStore.setTitle("Copy Path"); 97 | copyTextStore.setContent(pagePath); 98 | copyTextStore.show(); 99 | } 100 | 101 | // navigator.platform.includes("Mac") 102 | } 103 | 104 | export async function rename(pageName: string) { 105 | const currentPage = await logseq.Editor.getCurrentPage(); 106 | if (currentPage) { 107 | await logseq.Editor.renamePage(currentPage.name, pageName); 108 | logseq.UI.showMsg(`Page renamed to ${pageName}`); 109 | } else { 110 | logseq.UI.showMsg("Rename command only work on a page."); 111 | } 112 | } 113 | 114 | export function write() { 115 | logseq.UI.showMsg("Actually Logseq save your info automatically!"); 116 | } 117 | 118 | export function writeAndQuit() { 119 | logseq.UI.showMsg( 120 | "Actually Logseq save your info automatically! So just quit VIM command mode." 121 | ); 122 | hideMainUI(); 123 | } 124 | 125 | export function quit() { 126 | logseq.UI.showMsg("Quit VIM command mode."); 127 | hideMainUI(); 128 | } 129 | 130 | export async function substituteBlock(value) { 131 | const splitReplace = value.trim().split("/"); 132 | const search = splitReplace[1]; 133 | if (search) { 134 | const replace = parseEscapeSequences(splitReplace[2]) || ""; 135 | const modifiers = splitReplace[3] || ""; 136 | const regex = new RegExp(search, modifiers); 137 | const block = await logseq.Editor.getCurrentBlock(); 138 | if (block && block.uuid && block.content) { 139 | await replaceBlock(block, regex, replace); 140 | await logseq.UI.showMsg( 141 | 'Current block replaced "' + search + '" with "' + replace + '"' 142 | ); 143 | hideMainUI(); 144 | } else { 145 | await logseq.UI.showMsg( 146 | "Current block not found. Please select a block first." 147 | ); 148 | } 149 | } else { 150 | await logseq.UI.showMsg('Please input "s/search/replace/modifiers"'); 151 | } 152 | } 153 | 154 | export async function substitutePage(value) { 155 | const blocks = await logseq.Editor.getCurrentPageBlocksTree(); 156 | if (blocks.length > 0) { 157 | const splitReplace = value.trim().split("/"); 158 | const search = splitReplace[1]; 159 | if (search) { 160 | const replace = parseEscapeSequences(splitReplace[2]) || ""; 161 | const modifiers = splitReplace[3] || ""; 162 | const regex = new RegExp(search, modifiers); 163 | await walkReplace(blocks, regex, replace); 164 | await logseq.UI.showMsg( 165 | 'Current page blocks replaced "' + search + '" with "' + replace + '"' 166 | ); 167 | hideMainUI(); 168 | } else { 169 | await logseq.UI.showMsg('Please input "%s/search/replace/modifiers"'); 170 | } 171 | } else { 172 | await logseq.UI.showMsg( 173 | "Current page blocks not found. Please select a page first." 174 | ); 175 | } 176 | } 177 | 178 | export async function clearHighlights() { 179 | await clearCurrentPageBlocksHighlight(); 180 | } 181 | -------------------------------------------------------------------------------- /src/stores/color.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from "pinia"; 2 | import "@logseq/libs"; 3 | export const useColorStore = defineStore("color", { 4 | state: () => ({ 5 | visible: false, 6 | hide: true, 7 | color: "#fff", 8 | suckerCanvas: null, 9 | suckerArea: [], 10 | isSucking: false, 11 | timer: null, 12 | namedColors: { 13 | black: "#000000", 14 | silver: "#c0c0c0", 15 | gray: "#808080", 16 | white: "#ffffff", 17 | maroon: "#800000", 18 | red: "#ff0000", 19 | purple: "#800080", 20 | fuchsia: "#ff00ff", 21 | green: "#008000", 22 | lime: "#00ff00", 23 | olive: "#808000", 24 | yellow: "#ffff00", 25 | navy: "#000080", 26 | blue: "#0000ff", 27 | teal: "#008080", 28 | aqua: "#00ffff", 29 | orange: "#ffa500", 30 | aliceblue: "#f0f8ff", 31 | antiquewhite: "#faebd7", 32 | aquamarine: "#7fffd4", 33 | azure: "#f0ffff", 34 | beige: "#f5f5dc", 35 | bisque: "#ffe4c4", 36 | blanchedalmond: "#ffebcd", 37 | blueviolet: "#8a2be2", 38 | brown: "#a52a2a", 39 | burlywood: "#deb887", 40 | cadetblue: "#5f9ea0", 41 | chartreuse: "#7fff00", 42 | chocolate: "#d2691e", 43 | coral: "#ff7f50", 44 | cornflowerblue: "#6495ed", 45 | cornsilk: "#fff8dc", 46 | crimson: "#dc143c", 47 | cyan: "#00ffff", 48 | darkblue: "#00008b", 49 | darkcyan: "#008b8b", 50 | darkgoldenrod: "#b8860b", 51 | darkgray: "#a9a9a9", 52 | darkgreen: "#006400", 53 | darkgrey: "#a9a9a9", 54 | darkkhaki: "#bdb76b", 55 | darkmagenta: "#8b008b", 56 | darkolivegreen: "#556b2f", 57 | darkorange: "#ff8c00", 58 | darkorchid: "#9932cc", 59 | darkred: "#8b0000", 60 | darksalmon: "#e9967a", 61 | darkseagreen: "#8fbc8f", 62 | darkslateblue: "#483d8b", 63 | darkslategray: "#2f4f4f", 64 | darkslategrey: "#2f4f4f", 65 | darkturquoise: "#00ced1", 66 | darkviolet: "#9400d3", 67 | deeppink: "#ff1493", 68 | deepskyblue: "#00bfff", 69 | dimgray: "#696969", 70 | dimgrey: "#696969", 71 | dodgerblue: "#1e90ff", 72 | firebrick: "#b22222", 73 | floralwhite: "#fffaf0", 74 | forestgreen: "#228b22", 75 | gainsboro: "#dcdcdc", 76 | ghostwhite: "#f8f8ff", 77 | gold: "#ffd700", 78 | goldenrod: "#daa520", 79 | greenyellow: "#adff2f", 80 | grey: "#808080", 81 | honeydew: "#f0fff0", 82 | hotpink: "#ff69b4", 83 | indianred: "#cd5c5c", 84 | indigo: "#4b0082", 85 | ivory: "#fffff0", 86 | khaki: "#f0e68c", 87 | lavender: "#e6e6fa", 88 | lavenderblush: "#fff0f5", 89 | lawngreen: "#7cfc00", 90 | lemonchiffon: "#fffacd", 91 | lightblue: "#add8e6", 92 | lightcoral: "#f08080", 93 | lightcyan: "#e0ffff", 94 | lightgoldenrodyellow: "#fafad2", 95 | lightgray: "#d3d3d3", 96 | lightgreen: "#90ee90", 97 | lightgrey: "#d3d3d3", 98 | lightpink: "#ffb6c1", 99 | lightsalmon: "#ffa07a", 100 | lightseagreen: "#20b2aa", 101 | lightskyblue: "#87cefa", 102 | lightslategray: "#778899", 103 | lightslategrey: "#778899", 104 | lightsteelblue: "#b0c4de", 105 | lightyellow: "#ffffe0", 106 | limegreen: "#32cd32", 107 | linen: "#faf0e6", 108 | magenta: "#ff00ff", 109 | mediumaquamarine: "#66cdaa", 110 | mediumblue: "#0000cd", 111 | mediumorchid: "#ba55d3", 112 | mediumpurple: "#9370db", 113 | mediumseagreen: "#3cb371", 114 | mediumslateblue: "#7b68ee", 115 | mediumspringgreen: "#00fa9a", 116 | mediumturquoise: "#48d1cc", 117 | mediumvioletred: "#c71585", 118 | midnightblue: "#191970", 119 | mintcream: "#f5fffa", 120 | mistyrose: "#ffe4e1", 121 | moccasin: "#ffe4b5", 122 | navajowhite: "#ffdead", 123 | oldlace: "#fdf5e6", 124 | olivedrab: "#6b8e23", 125 | orangered: "#ff4500", 126 | orchid: "#da70d6", 127 | palegoldenrod: "#eee8aa", 128 | palegreen: "#98fb98", 129 | paleturquoise: "#afeeee", 130 | palevioletred: "#db7093", 131 | papayawhip: "#ffefd5", 132 | peachpuff: "#ffdab9", 133 | peru: "#cd853f", 134 | pink: "#ffc0cb", 135 | plum: "#dda0dd", 136 | powderblue: "#b0e0e6", 137 | rosybrown: "#bc8f8f", 138 | royalblue: "#4169e1", 139 | saddlebrown: "#8b4513", 140 | salmon: "#fa8072", 141 | sandybrown: "#f4a460", 142 | seagreen: "#2e8b57", 143 | seashell: "#fff5ee", 144 | sienna: "#a0522d", 145 | skyblue: "#87ceeb", 146 | slateblue: "#6a5acd", 147 | slategray: "#708090", 148 | slategrey: "#708090", 149 | snow: "#fffafa", 150 | springgreen: "#00ff7f", 151 | steelblue: "#4682b4", 152 | tan: "#d2b48c", 153 | thistle: "#d8bfd8", 154 | tomato: "#ff6347", 155 | turquoise: "#40e0d0", 156 | violet: "#ee82ee", 157 | wheat: "#f5deb3", 158 | whitesmoke: "#f5f5f5", 159 | yellowgreen: "#9acd32", 160 | rebeccapurple: "#663399", 161 | }, 162 | colorsDefault: [ 163 | "#000000", 164 | "#533e7d", 165 | "#497d46", 166 | "#787f97", 167 | "#978626", 168 | "#49767b", 169 | "#264c9b", 170 | "#793e3e", 171 | ], 172 | }), 173 | 174 | actions: { 175 | show() { 176 | this.visible = true; 177 | }, 178 | 179 | hide() { 180 | this.visible = false; 181 | }, 182 | 183 | async changeColor(color) { 184 | const { r, g, b, a } = color.rgba; 185 | this.color = `rgba(${r}, ${g}, ${b}, ${a})`; 186 | 187 | if (this.timer) { 188 | clearTimeout(this.timer); 189 | } 190 | 191 | this.timer = setTimeout(async () => { 192 | const selected = await logseq.Editor.getSelectedBlocks(); 193 | if (selected && selected.length > 1) { 194 | const mapped = selected.map((block) => 195 | logseq.Editor.upsertBlockProperty( 196 | block.uuid, 197 | "background-color", 198 | this.color 199 | ) 200 | ); 201 | await Promise.all(mapped); 202 | } else { 203 | const block = await logseq.Editor.getCurrentBlock(); 204 | if (block) { 205 | await logseq.Editor.upsertBlockProperty( 206 | block.uuid, 207 | "background-color", 208 | this.color 209 | ); 210 | } 211 | } 212 | 213 | this.timer = null; 214 | }, 300); 215 | }, 216 | }, 217 | }); 218 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## v0.2.0 4 | 5 | - fix: add `shift+.` and `shift+,` to indent and outdent block, thanks to @primeapple for the suggestion. #34 6 | - fix: use `shift+h` and `shift+l` to focus in and out block level. 7 | - fix: adjust showMsg to use logseq.UI.showMsg instead of logseq.App.showMsg 8 | - feat: search highlight can match word multi times in block 9 | - feat: search highlight can use a/A/i/I/d c to edit like VIM 10 | - fix: adjust some key bindings. 11 | - feat: big feature, add cursor to block in normal mode. 12 | - fix: optimize j and k to move across block levels. 13 | 14 | ## v0.1.24 15 | 16 | - chore: adjust github action config file 17 | 18 | ## v0.1.23 19 | 20 | - merge #65 and #66, thanks to @primeapple 21 | - fix: optimize vim command substitute 22 | 23 | ## v0.1.22 24 | 25 | - fix: hide Visual/Normal indicator by setting 26 | 27 | ## v0.1.21 28 | 29 | - fix: try to stay in the original page. 30 | 31 | ## v0.1.20 32 | 33 | - fix: jump into tag scenario 34 | - infra: upgrade deps 35 | 36 | ## v0.1.19 37 | 38 | - fix: optimize prevSibling and nextSibling action. It can keep on block page now. 39 | 40 | ## v0.1.18 41 | 42 | - feat: add delete prev and next blocks keybindings. #39 43 | - feat: add an option to show recent emoji by default. By the way, the default shortcut is `cmd+/` for triiger the emoji panel. #35 44 | - fix: jump into can not recognize tag correctly. #42 45 | - fix: try to fix go sibling commands not work in zoom edit mode. #41 46 | 47 | ## v0.1.17 48 | 49 | - fix: highlight focus in not work 50 | - adjust: replace `hl` and `HL` 51 | 52 | ## v0.1.16 53 | 54 | - fix typo #26 55 | 56 | ## v0.1.15 57 | 58 | - feat: add visual mode indicator 59 | - fix: highlight search collapsed content 60 | 61 | ## v0.1.14 62 | 63 | - fix: search highlight issues 64 | 65 | ## v0.1.13 66 | 67 | - fix: search highlight case 68 | 69 | ## v0.1.12 70 | 71 | - feat: refactor search highlight, now it won't change block content. 72 | 73 | ## v0.1.11 74 | 75 | - fix: change mapping `zm` and `zM` to `zc` and `zC`. #21 76 | - fix: adjust `a`, `i`, `A` and `I` mapping #20. 77 | - fix: typos, thanks to @RomanHN 78 | - fix: adjust color picker, auto lose focus for preview 79 | 80 | ## v0.1.10 81 | 82 | - feat: support `:NUMBER` to scroll to specific line or `:.NUMBER` represents scrolling to NUMBER \* 100% of the page. 83 | - fix: optimize search a little bit. 84 | 85 | ## v0.1.9 86 | 87 | - feat: add `--ns` and `--namespace` to `:go` and `:go!`, so you can go next level without inputing prefix title 88 | 89 | ## v0.1.8 90 | 91 | - feat: add `z M` to collapse hierarchically. add `z O` to extend hierarchically. 92 | 93 | ## v0.1.7 94 | 95 | - feat: new VIM-like in-page search support smartcase. 96 | - feat: add short n for next match, N for prev match. 97 | - feat: add highlight support, `:clear-highlights` to clear all highlights on current page. 98 | 99 | ## v0.1.6 100 | 101 | - feat: add `ctrl+a` and `ctrl+x` to increase or decrease the first found number in block. Support multiple selections and combo. 102 | - feat: add `x` and `X` to cut a leading character or word. Support multiple selections. 103 | - adjust: `:lorem` now support `-p` and `--paragraph` stands for `paragraph`, `-w` and `--word` stands for word. 104 | - adjust: `:lorem-ipsum` is deleted, because it is too long to type. 105 | 106 | ## v0.1.5 107 | 108 | - feat: add `:copy-path` to get page or journal absolute path, so you can edit it outside of Logseq. 109 | - feat: add `:open-in-vscode` to open page or journal in VSCode 110 | 111 | ## v0.1.4 112 | 113 | This release is all about setting block background color 114 | 115 | - feat: `:bg [namedColor|hexColor]` command to set block background color, support multiple block selection. 116 | - feat: `:bg-picker` command to trigger a color picker to select block background color, support multiple block selection. 117 | - feat: `:bg-random` command to set block background color randomly, support multiple block selection. 118 | - feat: `:bg-clear` command to clear block background color, support multiple block selection. 119 | - feat: `/Random Bg Color` and `Children Random Bg Color` to set block background color and children blocks background color. 120 | 121 | ## v0.1.3 122 | 123 | - feat: add `mod+alt+;` shortcut to command mode for Windows trigger. 124 | - feat: support emoji replacement, like `:smile:` will be replaced to 😄 immediately. 125 | 126 | ## v0.1.2 127 | 128 | - feat: add `:sort` and `:rsort` to sort blocks, it support to sort/reverse sort page first level blocks with no focus any blocks and to sort/reverse sort sub level blocks with focus on one block. 129 | - feat: also you can sort sub level blocks or reverse sort by slash command: `/Sort Blocks` and `/Reverse Sort Blocks`. 130 | - fix: change emoji-picker ui shortcut from `ctrl+e` to `mod+/` because `ctrl+e` in editing mode have special meaning. 131 | - fix: remove `trigger-on-focus` option commands. 132 | 133 | ## v0.1.1 134 | 135 | - feat: add `@back` and `@forward` for command mode `:go` command. 136 | - feat: add `:lorem` command to generate some random blocks. 137 | - feat: add `:emoji` to insert emojis by searching keyword. 138 | - feat: add `:emoji-picker` to open emoji picker UI. 139 | - feat: add `ctrl+e` to trigger emoji picker UI. 140 | - feat: add `/Insert emoji` slash command to trigger emoji picker UI. 141 | 142 | ## v0.1.0 143 | 144 | - feat: add VIM command mode, trigger by shortcut: `mod+shift+;`,for now 10+ commands supported, and I'm sure that would be more. 145 | - feat: one useful commmand is: replace string like VIM. Input `:%s/search/replace/modifiers`, e.g. `:%s/foo/bar/ig` 146 | - feat: another useful command is: go to page. Input `:go PAGENAME`, e.g. `:go 2022-02-22`, also support go to block by `:go ((blockId))` 147 | - feat: some commands related to marks: `:marks`, `:delmarks`, `:delmarks!`, `:mark`. 148 | - more commands descriptions in README. 149 | 150 | ## v0.0.8 151 | 152 | - fix: `ctrl+v` in Windows is for pasting, so I disable visual block mode key-binding for editing mode. 153 | - feat: add mark feature like VIM, the short cut is `NUMBER + m` to save current page or block, and `NUMBER + '` to load saved mark, and `mod+'` to load saved mark on right sidebar. The NUMBER can be more than 10, actually thousands if you wish. 154 | - infra: build tool changed from Webpack to Vite. 155 | - infra: use Github Actions to publish plugin. 156 | 157 | ## v0.0.7 158 | 159 | - feat: add changing case action, the shortcut is `mod+shift+u`, means to toggle upper case and lower case. 160 | - feat: combo action supported 16 case style, `Number key` + `mod+shift+u` to trigger, learn more from README. 161 | - feat: add original VIM case shortcut, `gu` is for lower case, `gU` is for upper case. 162 | 163 | ## v0.0.6 164 | 165 | - feat: add VIM-like visual block mode, in this mode, `j` and `k` are for block selecting, `J` and `K` are for block moving. 166 | 167 | ## v0.0.5 168 | 169 | - all actions support multiple key bindings in settings JSON file. 170 | 171 | ## v0.0.4 172 | 173 | - fix: change `ctrl+enter` to `mod+shift+enter` to jump internal page or tag. 174 | 175 | ## v0.0.3 176 | 177 | - fix: `mod+j mod+j` would conflict with `ctrl+j` on many devices, so I changed `ctrl+j` shortcut to `mod+alt+j` to join sibling block. 178 | - feat: add `ctrl+[` to also trigger exiting editing mode, but still have `mod+j mod+j`. 179 | - feat: add `a` to enter insert mode, but still have `i` do the same thing. 180 | 181 | ## v0.0.2 182 | 183 | - feat: add `mod+j mod+j` to exit editing mode. 184 | - feat: add `ctrl+enter` to jump into internal page, support `[[]]` and `#tag` style, sometimes better UX than DWIM. 185 | - feat: add `ctrl+j` to join next sibling conditionally. 186 | - fix: `zo` and `zm` shortcut not work on latest Logseq release. 187 | 188 | NOTE: 189 | 190 | 1. The joining shortcut can only join siblings without children blocks and support combo and should not be trigger too fast in editing mode. 191 | 2. Jumping internal page support combo to select which page to jump. 192 | -------------------------------------------------------------------------------- /src/components/Command.vue: -------------------------------------------------------------------------------- 1 | 299 | 300 | 333 | 334 | 339 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "esnext" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 15 | "lib": [ 16 | "ESNext", 17 | "DOM" 18 | ] /* Specify a set of bundled library declaration files that describe the target runtime environment. */, 19 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 20 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 21 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 22 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ 23 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 24 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ 25 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ 26 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 27 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 28 | 29 | /* Modules */ 30 | "module": "commonjs" /* Specify what module code is generated. */, 31 | // "rootDir": "./", /* Specify the root folder within your source files. */ 32 | // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 33 | "baseUrl": "." /* Specify the base directory to resolve non-relative module names. */, 34 | "paths": { 35 | "@/*": ["src/*"] 36 | } /* Specify a set of entries that re-map imports to additional lookup locations. */, 37 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 38 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ 39 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 40 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 41 | "resolveJsonModule": true /* Enable importing .json files */, 42 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ 43 | 44 | /* JavaScript Support */ 45 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ 46 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 47 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ 48 | 49 | /* Emit */ 50 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 51 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 52 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 53 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 54 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ 55 | // "outDir": "./", /* Specify an output folder for all emitted files. */ 56 | // "removeComments": true, /* Disable emitting comments. */ 57 | // "noEmit": true, /* Disable emitting files from a compilation. */ 58 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 59 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ 60 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 61 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 62 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 63 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 64 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 65 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 66 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 67 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ 68 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ 69 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 70 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ 71 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 72 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 73 | 74 | /* Interop Constraints */ 75 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 76 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 77 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */, 78 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 79 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 80 | 81 | /* Type Checking */ 82 | // "strict": true, /* Enable all strict type-checking options. */ 83 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ 84 | // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ 85 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 86 | // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ 87 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 88 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ 89 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ 90 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 91 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ 92 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ 93 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 94 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 95 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 96 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 97 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 98 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ 99 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 100 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 101 | 102 | /* Completeness */ 103 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 104 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [Get me a coffee](https://www.buymeacoffee.com/vipzhicheng) if you like this plugin! 2 | 3 | # logseq-plugin-vim-shortcuts 4 | 5 | [![Github All Releases](https://img.shields.io/github/downloads/vipzhicheng/logseq-plugin-vim-shortcuts/total.svg)](https://github.com/vipzhicheng/logseq-plugin-vim-shortcuts/releases) 6 | 7 | This plugin provide some shortcuts which give Logseq a VIM-like feeling. 8 | 9 | ![screencast](screencast.gif) 10 | 11 | ## Supported shortcuts 12 | 13 | - `j`: Move to next line. In visual block mode, it is for selecting down. 14 | - `k`: Move to previous line. In visual block mode, it is for selecting up. 15 | - `H`: Highlight focus out to parent level. 16 | - `L`: Highlight focus into child level. 17 | - `J`: Move to next sibling. In visual block mode, it is for moving down. 18 | - `K`: Move to previous sibling. In visual block mode, it is for moving up. 19 | - `<`: Outdent. 20 | - `>`: Indent. 21 | - `a` and `A`: Move the cursor to the end and enter edit mode. 22 | - `i` and `I`: Move the cursor to the beginning and enter edit mode. 23 | - `yy`: Copy current block content. Only supports one block – for copying multiple blocks, please use `cmd+c`. 24 | - `Y`: Copy current block ref. 25 | - `p`: Paste clipboard content to next sibling. Only supports one block – for pasting multiple blocks, please use `cmd+v`. 26 | - `P`: Paste clipboard content to previous sibling. Only supports one block – for pasting multiple blocks, please use `cmd+v`. 27 | - `o`: Insert an empty block to next sibling. 28 | - `O`: Insert an empty block to previous sibling. 29 | - `dd`: Delete current block. Child blocks will also be deleted, but only current block content in the clipboard. 30 | - `dc`: Change current block (delete content and change into edit mode). For selection the first block is changed, all the others are deleted, but only the top blocks content goes into the clipboard. 31 | - `dj`: Delete current and next blocks. Child blocks will also be deleted, but only current block content in the clipboard. 32 | - `dk`: Delete current and prev blocks. Child blocks will also be deleted, but only current block content in the clipboard. 33 | - `T`: Scroll to top, because Logseq uses `gg` to go to graph view. 34 | - `G`: Scroll to bottom. 35 | - `u`: Undo. 36 | - `ctrl+r`: Redo. 37 | - `gu`: Change block content to lower case. 38 | - `gU`: Change block content to upper case. 39 | - `mod+shift+u`: Toggle block content between lower and upper case. 40 | - `NUMBER`+`mod+shift+u`: Trigger different case style, supports 1–16. 41 | - `zo`: Extend block. 42 | - `zc`: Collapse block. 43 | - `zO`: Extend block hierarchically. 44 | - `zC`: Collapse block hierarchically. 45 | - `NUMBER`+`m`: Save current page or block as a mark to `NUMBER` register. 46 | - `NUMBER`+`'`: Load saved mark on main region. 47 | - `NUMBER`+`mod+'`: Load saved mark on right sidebar. 48 | - `cmd+j cmd+j`: Exit editing mode. `ctrl+[` does the same thing. 49 | - `mod+alt+j`: Join next sibling block. 50 | - `mod+shift+enter`: Jumping into internal page or tag. 51 | - `mod+shift+;` and `mod+alt+;`: Trigger command mode. This provides many handy commands to use, explained below. 52 | - `ctrl+a`: Increase the first found number in block. Supports multiple selections and combo. 53 | - `ctrl+x`: Decrease the first found number in block. Supports multiple selections and combo. 54 | - `x`: Cut a leading character. Supports multiple selections. 55 | - `X`: Cut a leading word. Supports multiple selections. 56 | - `/`: Trigger search in page bar on the below. Supports smartcase. 57 | - `n`: Search next search match. 58 | - `N`: Search previous search match. 59 | - `sb`: Search block content in Baidu. 60 | - `se`: Search block content in Wikipedia. 61 | - `sg`: Search block content in Google. 62 | - `sh`: Search block content in Github. 63 | - `ss`: Search block content in Stackoverflow. 64 | - `sy`: Search block content in Youtube. 65 | - `ctrl+v`: Toggle visual block mode. 66 | - `mod+/`: Trigger emoji picker UI. 67 | 68 | ## Modes 69 | 70 | ### Normal mode 71 | 72 | A block is focused/highlighted. 73 | 74 | ### Insert mode 75 | 76 | You can edit a block. 77 | 78 | ### Visual block mode 79 | 80 | You can select more blocks up and down and move the selected blocks using `j` and `k`. 81 | 82 | ### Command mode 83 | 84 | In VIM this mode can be triggered by `:`, but here, the shortcut is `mod+shift+;`, also can be memorized as `mod+:`. After trigger, you can find an input area at the bottom, you can input some commands here like in VIM. For now it's about 10+ commands, but I believe that would be more. 85 | 86 | NOTE: on Windows, the trigger is `ctrl+alt+;` 87 | 88 | #### The bottom input features 89 | 90 | - Autosuggestion when you input. 91 | - Press `Tab` if only one command matched, the matched command will be autocompleted right away. 92 | - Press `Up` and `Down` to traverse command history, it's a 1000 limit history, I think it's enough to use. 93 | - Press `Esc` to close command mode and back to the main window. For now Logseq can not get focused automatically sometime. so you need to click the main window to continue. 94 | - Just in case bug stuck, there are `Run` button and `Close` at bottom right to help you trigger behaviors. 95 | 96 | #### Supported commands 97 | 98 | - `:NUMBER` to scroll to specific line or `:-NUMBER` to scroll to specific line to the end or `:.NUMBER` represents scrolling to NUMBER \* 100% of the page. 99 | - `:s/` and `:substitute/`: Replace current block according regex, e.g. `s/foo/bar/gi`, Notice it support Regex modifiers. 100 | - `:%s/`a nd `:%substitute/`: Replace current page blocks according regex, e.g. `%s/foo/bar/gi`. 101 | - `:marks`: Show marks. 102 | - `:delm` and `:delmarks`: Delete specific mark ids, e.g. `:delm 1 2 3`. 103 | - `:delm!` and `:delmarks!`: Delete all marks. 104 | - `:m` and `:mark`: Go to specific mark, e.g. `:m 1`. 105 | - `:go`: Go to existed page or block, e.g. `:go 2022-02-22` or `:go ((6219c981-256a-4464-bc62-4ecfab4c2141))`. 106 | - There are some shortcuts for page name: 107 | - `:go @` and `:go @index`: Go to Contents page 108 | - `:go @today` Go to today's journal page. 109 | - `:go @yesterday` Go to yesterday's journal page. 110 | - `:go @tomorrow` Go to tomorrow's journal page. 111 | - `:go @prev` Go to prev-day's journal page, if currect page is not a journal page, fallback to @yesterday. 112 | - `:go @next` Go to next-day's journal page, if currect page is not a journal page, fallback to @tomorrow. 113 | - `:go @back` Go to backward page. 114 | - `:go @forward` Go to forward page. 115 | - `:go!`: Go to existed page or block, create one if page not exist, e.g. `:go 2022-02-22` or `:go ((6219c981-256a-4464-bc62-4ecfab4c2141))`. 116 | - `:go!` and `:go` support `--ns` and `--namespace` to go or create namespace page. e.g. you run command `:go! subpage --ns` on `test` page, then you will be redirect to `test/subpage` for saving your time to input prefix chars. 117 | - `:re` and `:rename`: Rename current page name, if target page exists, page content will be merged. 118 | - `:undo` and `:redo`: Undo and redo last edit. 119 | - `:lorem` and `:lorem-ipsum`: generate random blocks in same level, with `--unit word|paragraph|sentence` to change random block unit. 120 | - `--unit` has a short style as `-u` 121 | - `word`, `paragraph` and `sentence` also have short styles as `w`, `p`, and `s`. 122 | - also support `-p`, `-s`, `-w` and `--paragraph`, `--sentence`, `--word`. 123 | - `:emoji`: Insert emojis by searching keyword, you can repeat emoji by appending a number. 124 | - `:emoji-picker`: Insert emoji by emoji UI. 125 | - `:sort` and `rsort`: Sort page first level blocks with no focus any blocks and sort sub level blocks with focus on one block. 126 | - `:bg [namedColor|hexColor]`: Set block background color, support multiple block selection. 127 | - `:bg-picker`: Trigger a color picker to select block background color, support multiple block selection. 128 | - `:bg-random`: Set block background color randomly, support multiple block selection. 129 | - `:bg-clear`: Clear block background color, support multiple block selection. 130 | - `:copy-path`: Get page or journal absolute path, so you can edit it outside of Logseq. 131 | - `:open-in-vscode`: Open page or journal in VSCode. 132 | - `:w` and `:write`: Save current page, actually this is a fake one, because Logseq save automatically. 133 | - `:wq`: Save current page and quit vim command mode. 134 | - `:q` and `:quit`: Quit vim command mode. 135 | - `h` and `help`: Show a help message modal. 136 | 137 | ## Case Change Styles 138 | 139 | - `1`: Toggle upper and lower case, the default behavior. 140 | - `2`: Change to upper case. e.g. `LOGSEQ IS SO AWESOME` 141 | - `3`: Change to lower case. e.g. `logseq is so awesome` 142 | - `4`: Change to title case. e.g. `Logseq Is so Awesome` 143 | - `5`: Change to sentence case. e.g. `Logseq is so awesome` 144 | - `6`: Change to path case. e.g. `logseq/is/so/awesome` 145 | - `7`: Change to capital case. e.g. `Logseq Is So Awesome` 146 | - `8`: Change to constant case. e.g. `LOGSEQ_IS_SO_AWESOME` 147 | - `9`: Change to dot case. e.g. `logseq.is.so.awesome` 148 | - `10`: Change to header case. e.g. `Logseq-Is-So-Awesome` 149 | - `11`: Change to param case. e.g. `logseq-is-so-awesome` 150 | - `12`: Change to pascal case. e.g. `LogseqIsSoAwesome` 151 | - `13`: Change to camel case. e.g. `logseqIsSoAwesome` 152 | - `14`: Change to snake case. e.g. `logseq_is_so_awesome` 153 | - `15`: Change to swap case. e.g. `lOGSEQ IS SO AWESOME` 154 | - `16`: Change to random case. e.g. `logsEQ IS SO awESoME` 155 | 156 | ## Mark feature notes 157 | 158 | - Logseq have `Favorites` and `Recent` feature, and we also have a `Tabs` plugins, the mark feature kind of like Tabs position, but give the power to yourself to decide which is which, that feels good to me. 159 | - Marked pages and blocks can be persisted automatically and graph separately. 160 | - NUMBER can be more than 10, actually thousands if you wish. 161 | - The `m` shortcut could be conflicted with `Markmap` plugin, so if you met this issue, please upgrade `Markmap` plugin to latest version. 162 | - NUMBER=1 is the default one, so you can just press `m` to save and press `'` to load for mark 1. 163 | 164 | ## Slash commands this plugin added 165 | 166 | Because some VIM shortcuts or commands may also need to be as a slash command, so this plugin provides some. 167 | 168 | - `/Insert Emoji`: Insert emoji at current position. 169 | - `/Sort Blocks`: Sort sub level children blocks from a to z. 170 | - `/Reverse Sort Blocks`: Sort sub level children blocks from z to a. 171 | - `/Random Bg Color`: Set block background color 172 | - `/Children Random Bg Color`: Set children blocks background color 173 | 174 | ## Named Background Colors 175 | 176 | The named colors you can use in `:bg` command are from [here](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value) 177 | 178 | ## Search in page 179 | 180 | Like in VIM, if you press `/` you can trigger in-page search, and it support smartcase which means if you search in lower case it will match case insensitive and if you search a keyword including upper case char then it will match case sensitive. 181 | 182 | ## Other notes 183 | 184 | - Logseq keybindings support may be changed in future, so just use it for a while if you need it, and it may be conflicted with Logseq future shortcuts. 185 | - Not exactly same with VIM key-bindings, just mimic. 186 | - If you are on journal home page, some shortcuts will redirect you to specific page, because there is no API can stay journal home page and move block highlight line. 187 | - Some shortcuts are not perfect for now, maybe need more polish and some support from Logseq Team. 188 | - There may be more shortcuts coming soon. 189 | - Stay tuned. 190 | - Copy here not means copy to system clipboard, just in memory of Logseq. 191 | - The `VIM` scroll to top shortcut is `gg`, if you want it, you can change Logseq gg shortcut to another one, and set gg in plugin settings JSON file. 192 | - Some shortcuts support VIM-like combo actions, that means pressing `N+action` to run action `N` times. 193 | - The join shortcuts can only join siblings without children blocks and should not be trigger too fast in editing mode. 194 | - Jumping internal page support combo action to select which page to jump. 195 | - All actions support multiple key bindings in settings JSON file. 196 | - `mod` means `Cmd` on Mac, `Ctrl` on Windows/Linux. 197 | - Recommend version of Logseq is `v0.5.9`+. 198 | 199 | ## ❤️ Buy me a coffee 200 | 201 | If this plugin solve your situation a little bit and you will, you can choose to buy me a coffee via [this](https://www.buymeacoffee.com/vipzhicheng) and [this](https://afdian.net/@vipzhicheng), that means a lot to me. 202 | 203 | ## Licence 204 | 205 | MIT 206 | --------------------------------------------------------------------------------