/* THIS IS A GENERATED/BUNDLED FILE BY ESBUILD if you want to view the source, please visit the github repository of this plugin */ "use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/main.ts var main_exports = {}; __export(main_exports, { default: () => BindThemPlugin }); module.exports = __toCommonJS(main_exports); var import_obsidian3 = require("obsidian"); // src/settings.ts var import_obsidian = require("obsidian"); // src/Constants.ts var DEBUG_HEAD = "[BindThem] "; var PRE_WIDOW_EXPAND_CHARS = "#$@"; var POST_WIDOW_EXPAND_CHARS = "!?:"; var DEFAULT_SENTENCE_REGEX = `[^.!?\\s][^.!?]*(?:[.!?](?!['"]?\\s|$)[^.!?]*)*[.!?]?['"]?(?=\\s|$)`; var LIST_CHARACTER_REGEX = /^\s*(-|\+|\*|\d+\.|>) (\[.\] )?$/; var HEADING_REGEX = /^(#*)( *)(.*)/; // src/settings.ts var COMMAND_CATEGORIES = { betterFormatting: "Better Formatting", directionalCopyMove: "Directional Copy/Move", toggleHeading: "Toggle Heading", sentenceNavigator: "Sentence Navigator", lineOperations: "Line Operations", caseTransformation: "Case Transformation", navigation: "Navigation", cursorMovement: "Cursor Movement", fileOperations: "File Operations", utility: "Utility" }; var COMMANDS = { betterFormatting: [ { id: "toggle-bold-underscore", name: "Toggle bold underscore" }, { id: "toggle-bold-asterisk", name: "Toggle bold asterisk" }, { id: "toggle-italics-underscore", name: "Toggle italics underscore" }, { id: "toggle-italics-asterisk", name: "Toggle italics asterisk" }, { id: "toggle-code", name: "Toggle code" }, { id: "toggle-comment", name: "Toggle comment" }, { id: "toggle-highlight", name: "Toggle highlight" }, { id: "toggle-strikethrough", name: "Toggle strikethrough" }, { id: "toggle-math-inline", name: "Toggle math inline" }, { id: "toggle-math-block", name: "Toggle math block" } ], directionalCopyMove: [ { id: "copy-up", name: "Copy up" }, { id: "copy-down", name: "Copy down" }, { id: "copy-left", name: "Copy left" }, { id: "copy-right", name: "Copy right" }, { id: "move-left", name: "Move left" }, { id: "move-right", name: "Move right" } ], toggleHeading: [ { id: "toggle-heading-1", name: "Toggle heading 1" }, { id: "toggle-heading-2", name: "Toggle heading 2" }, { id: "toggle-heading-3", name: "Toggle heading 3" }, { id: "toggle-heading-4", name: "Toggle heading 4" }, { id: "toggle-heading-5", name: "Toggle heading 5" }, { id: "toggle-heading-6", name: "Toggle heading 6" }, { id: "toggle-heading-strip-1", name: "Toggle heading 1 (strip formatting)" }, { id: "toggle-heading-strip-2", name: "Toggle heading 2 (strip formatting)" }, { id: "toggle-heading-strip-3", name: "Toggle heading 3 (strip formatting)" }, { id: "toggle-heading-strip-4", name: "Toggle heading 4 (strip formatting)" }, { id: "toggle-heading-strip-5", name: "Toggle heading 5 (strip formatting)" }, { id: "toggle-heading-strip-6", name: "Toggle heading 6 (strip formatting)" } ], sentenceNavigator: [ { id: "delete-to-start-of-sentence", name: "Delete to start of sentence" }, { id: "delete-to-end-of-sentence", name: "Delete to end of sentence" }, { id: "select-to-start-of-sentence", name: "Select to start of sentence" }, { id: "select-to-end-of-sentence", name: "Select to end of sentence" }, { id: "move-to-start-of-current-sentence", name: "Move to start of current sentence" }, { id: "move-to-start-of-next-sentence", name: "Move to start of next sentence" }, { id: "select-sentence", name: "Select current sentence" } ], lineOperations: [ { id: "insert-line-above", name: "Insert line above" }, { id: "insert-line-below", name: "Insert line below" }, { id: "delete-line", name: "Delete line" }, { id: "delete-to-start-of-line", name: "Delete to start of line" }, { id: "delete-to-end-of-line", name: "Delete to end of line" }, { id: "join-lines", name: "Join lines" }, { id: "duplicate-line", name: "Duplicate line" }, { id: "copy-line-up", name: "Copy line up" }, { id: "copy-line-down", name: "Copy line down" }, { id: "select-word", name: "Select word" }, { id: "select-line", name: "Select line" } ], caseTransformation: [ { id: "transform-uppercase", name: "Transform to uppercase" }, { id: "transform-lowercase", name: "Transform to lowercase" }, { id: "transform-titlecase", name: "Transform to title case" }, { id: "toggle-case", name: "Toggle case" } ], navigation: [ { id: "go-to-line-start", name: "Go to line start" }, { id: "go-to-line-end", name: "Go to line end" }, { id: "go-to-first-line", name: "Go to first line" }, { id: "go-to-last-line", name: "Go to last line" }, { id: "go-to-line-number", name: "Go to line number" }, { id: "go-to-next-heading", name: "Go to next heading" }, { id: "go-to-prev-heading", name: "Go to previous heading" } ], cursorMovement: [ { id: "move-cursor-up", name: "Move cursor up" }, { id: "move-cursor-down", name: "Move cursor down" }, { id: "move-cursor-left", name: "Move cursor left" }, { id: "move-cursor-right", name: "Move cursor right" }, { id: "go-to-prev-word", name: "Go to previous word" }, { id: "go-to-next-word", name: "Go to next word" }, { id: "insert-cursor-above", name: "Insert cursor above" }, { id: "insert-cursor-below", name: "Insert cursor below" }, { id: "select-next-occurrence", name: "Select next occurrence" }, { id: "select-prev-occurrence", name: "Select previous occurrence" }, { id: "select-all-occurrences", name: "Select all occurrences" } ], fileOperations: [ { id: "duplicate-file", name: "Duplicate file" }, { id: "new-adjacent-file", name: "New adjacent file" } ], utility: [ { id: "toggle-line-numbers", name: "Toggle line numbers" }, { id: "undo", name: "Undo" }, { id: "redo", name: "Redo" } ] }; function getAllCommandIds() { const ids = []; for (const commands of Object.values(COMMANDS)) { for (const cmd of commands) { ids.push(cmd.id); } } return ids; } function createDefaultEnabledCommands() { const enabled = {}; for (const cmdId of getAllCommandIds()) { enabled[cmdId] = true; } return enabled; } var DEFAULT_SETTINGS = { debug: false, sentenceRegexSource: DEFAULT_SENTENCE_REGEX, enabledCommands: createDefaultEnabledCommands() }; var BindThemSettingTab = class extends import_obsidian.PluginSettingTab { constructor(app, plugin) { super(app, plugin); this.plugin = plugin; } display() { const { containerEl } = this; containerEl.empty(); containerEl.createEl("a", { text: "Configure keyboard shortcuts", cls: "bindthem-hotkeys-link", attr: { href: "#", style: "display: inline-block; margin-bottom: 1em; color: var(--text-accent); cursor: pointer;" } }, (el) => { el.addEventListener("click", (e) => { e.preventDefault(); const hotkeysTab = this.app.setting.openTabById("hotkeys"); if (hotkeysTab) { hotkeysTab.searchComponent.setValue("BindThem"); } }); }); new import_obsidian.Setting(containerEl).setName("Debug mode").setDesc("Enable debug logging to console").addToggle((toggle) => toggle.setValue(this.plugin.settings.debug).onChange(async (value) => { this.plugin.settings.debug = value; await this.plugin.saveSettings(); })); new import_obsidian.Setting(containerEl).setName("Sentence regex").setDesc("Regular expression used to match sentences").addText((text) => text.setValue(this.plugin.settings.sentenceRegexSource).onChange(async (value) => { this.plugin.settings.sentenceRegexSource = value; await this.plugin.saveSettings(); })); new import_obsidian.Setting(containerEl).setName("Reset sentence regex").addButton((button) => button.setButtonText("Reset").onClick(async () => { this.plugin.settings.sentenceRegexSource = DEFAULT_SENTENCE_REGEX; await this.plugin.saveSettings(); this.display(); })); new import_obsidian.Setting(containerEl).setName("Command availability").setDesc("Toggle which commands are available in Obsidian. Disabled commands will not appear in the command palette.").setHeading(); new import_obsidian.Setting(containerEl).setName("Enable/disable all").setDesc("Toggle all commands on or off").addButton((button) => button.setButtonText("Enable all").onClick(async () => { for (const cmdId of getAllCommandIds()) { this.plugin.settings.enabledCommands[cmdId] = true; } await this.plugin.saveSettings(); this.display(); })).addButton((button) => button.setButtonText("Disable all").setWarning().onClick(async () => { for (const cmdId of getAllCommandIds()) { this.plugin.settings.enabledCommands[cmdId] = false; } await this.plugin.saveSettings(); this.display(); })); for (const [categoryKey, categoryName] of Object.entries(COMMAND_CATEGORIES)) { this.renderCategory(containerEl, categoryKey, categoryName); } new import_obsidian.Setting(containerEl).setName("About").setDesc("Combines features from obsidian-tweaks, obsidian-editor-shortcuts, heading-toggler, and obsidian-sentence-navigator.").setHeading(); } renderCategory(containerEl, categoryKey, categoryName) { const commands = COMMANDS[categoryKey]; if (!commands || commands.length === 0) return; const categorySetting = new import_obsidian.Setting(containerEl).setName(categoryName).setHeading(); const allEnabled = commands.every((cmd) => this.plugin.settings.enabledCommands[cmd.id] !== false); categorySetting.addToggle((toggle) => { toggle.setValue(allEnabled).setTooltip("Toggle all commands in this category").onChange(async (value) => { for (const cmd of commands) { this.plugin.settings.enabledCommands[cmd.id] = value; } await this.plugin.saveSettings(); this.display(); }); }); for (const cmd of commands) { new import_obsidian.Setting(containerEl).setName(cmd.name).setDesc(cmd.description || `Command ID: ${cmd.id}`).addToggle((toggle) => toggle.setValue(this.plugin.settings.enabledCommands[cmd.id] !== false).onChange(async (value) => { this.plugin.settings.enabledCommands[cmd.id] = value; await this.plugin.saveSettings(); })); } } }; // src/Utils.ts function getMainSelection(editor) { return { anchor: editor.getCursor("anchor"), head: editor.getCursor("head") }; } function selectionToRange(selection) { const sortedPositions = [selection.anchor, selection.head].sort((a, b) => { if (a.line !== b.line) return a.line - b.line; return a.ch - b.ch; }); return { from: sortedPositions[0], to: sortedPositions[1] }; } function selectionToLine(editor, selection) { const range = selectionToRange(selection); return { anchor: { line: range.from.line, ch: 0 }, head: { line: range.to.line, ch: editor.getLine(range.to.line).length } }; } function getLeadingWhitespace(lineContent) { const indentation = lineContent.match(/^\s+/); return indentation ? indentation[0] : ""; } function isLetterOrDigit(char) { return /\p{L}\p{M}*/u.test(char) || /\d/.test(char); } function wordRangeAtPos(pos, lineContent) { let start = pos.ch; let end = pos.ch; while (start > 0 && isLetterOrDigit(lineContent.charAt(start - 1))) start--; while (end < lineContent.length && isLetterOrDigit(lineContent.charAt(end))) end++; return { anchor: { line: pos.line, ch: start }, head: { line: pos.line, ch: end } }; } var LOWERCASE_ARTICLES = ["the", "a", "an"]; function toTitleCase(text) { return text.split(/(\s+)/).map((w, i, all) => { if (i > 0 && i < all.length - 1 && LOWERCASE_ARTICLES.includes(w.toLowerCase())) { return w.toLowerCase(); } return w.charAt(0).toUpperCase() + w.slice(1).toLowerCase(); }).join(" "); } // src/BetterFormatting.ts var BetterFormatting = class { constructor(app, plugin) { this.app = app; this.plugin = plugin; } /** * Expand a range by words, including intelligent handling of widow characters */ expandRangeByWords(editor, range, symbolStart, symbolEnd) { let from = (editor.wordAt(range.from) || range).from; let to = (editor.wordAt(range.to) || range).to; while (true) { const newFrom2 = { line: from.line, ch: from.ch - 1 }; const newTo2 = { line: to.line, ch: to.ch + 1 }; const preChar = editor.getRange(newFrom2, from); const postChar = editor.getRange(to, newTo2); if (preChar === postChar) { break; } if (symbolStart.endsWith(preChar)) { break; } if (!PRE_WIDOW_EXPAND_CHARS.includes(preChar)) { break; } from = newFrom2; } while (true) { const newFrom2 = { line: from.line, ch: from.ch - 1 }; const newTo2 = { line: to.line, ch: to.ch + 1 }; const preChar = editor.getRange(newFrom2, from); const postChar = editor.getRange(to, newTo2); if (preChar === postChar) { break; } if (symbolEnd.startsWith(postChar)) { break; } if (!POST_WIDOW_EXPAND_CHARS.includes(postChar)) { break; } to = newTo2; } const newFrom = { line: from.line, ch: from.ch - symbolStart.length }; const preString = editor.getRange(newFrom, from); if (preString === symbolStart) { from = newFrom; } const newTo = { line: to.line, ch: to.ch + symbolEnd.length }; const postString = editor.getRange(to, newTo); if (postString === symbolEnd) { to = newTo; } return { from, to }; } /** * Check if text in the given range is already wrapped with the specified symbols */ isWrapped(editor, range, symbolStart, symbolEnd) { const text = editor.getRange(range.from, range.to); return text.startsWith(symbolStart) && text.endsWith(symbolEnd); } /** * Create changes to wrap text with symbols */ wrap(editor, textRange, selection, symbolStart, symbolEnd) { const changes = [ { from: textRange.from, text: symbolStart }, { from: textRange.to, text: symbolEnd } ]; const newSelection = { anchor: { ...selection.anchor }, head: { ...selection.head } }; newSelection.anchor.ch += symbolStart.length; newSelection.head.ch += symbolEnd.length; return [changes, newSelection]; } /** * Create changes to unwrap text from symbols */ unwrap(editor, textRange, selection, symbolStart, symbolEnd) { const changes = [ { from: textRange.from, to: { line: textRange.from.line, ch: textRange.from.ch + symbolStart.length }, text: "" }, { from: { line: textRange.to.line, ch: textRange.to.ch - symbolEnd.length }, to: textRange.to, text: "" } ]; const newSelection = { anchor: { ...selection.anchor }, head: { ...selection.head } }; newSelection.anchor.ch -= symbolStart.length; newSelection.head.ch -= symbolStart.length; return [changes, newSelection]; } /** * Set the wrap state for a selection */ setSelectionWrapState(editor, selection, wrapState, symbolStart, symbolEnd) { const initialRange = selectionToRange(selection); const textRange = this.expandRangeByWords( editor, initialRange, symbolStart, symbolEnd ); const alreadyWrapped = this.isWrapped(editor, textRange, symbolStart, symbolEnd); if (alreadyWrapped === wrapState) { return [[], selection]; } if (wrapState) { return this.wrap(editor, textRange, selection, symbolStart, symbolEnd); } return this.unwrap(editor, textRange, selection, symbolStart, symbolEnd); } /** * Toggle wrapper symbols around the current selection(s) * * Principle: Toggling twice == no-op */ toggleWrapper(editor, view, symbolStart, symbolEnd) { if (symbolEnd === void 0) { symbolEnd = symbolStart; } const selections = editor.listSelections(); const mainSelection = getMainSelection(editor); const mainRange = this.expandRangeByWords( editor, selectionToRange(mainSelection), symbolStart, symbolEnd ); const isWrapped = this.isWrapped(editor, mainRange, symbolStart, symbolEnd); const targetWrapState = !isWrapped; const newChanges = []; const newSelectionRanges = []; for (const selection of selections) { const [changes, newSelection] = this.setSelectionWrapState( editor, selection, targetWrapState, symbolStart, symbolEnd ); newChanges.push(...changes); newSelectionRanges.push(selectionToRange(newSelection)); } const transaction = { changes: newChanges, selections: newSelectionRanges }; let origin = targetWrapState ? "+" : "-"; origin += "BetterFormatting_" + symbolStart + symbolEnd; editor.transaction(transaction, origin); } }; // src/DirectionalCopy.ts var DirectionalCopy = class { constructor(app, plugin) { this.app = app; this.plugin = plugin; } /** * Copy the current selection(s) in the specified direction */ directionalCopy(editor, view, direction) { const selections = editor.listSelections(); const vertical = direction === "Up" /* Up */ || direction === "Down" /* Down */; if (vertical) { selections.forEach((selection, idx, arr) => { arr[idx] = selectionToLine(editor, selection); }); } const changes = []; for (const selection of selections) { const range = selectionToRange(selection); const content = editor.getRange(range.from, range.to); let change; switch (direction) { case "Up" /* Up */: { change = { from: range.from, to: range.from, text: content + "\n" }; break; } case "Down" /* Down */: { change = { from: range.to, to: range.to, text: "\n" + content }; break; } case "Left" /* Left */: { change = { from: range.from, to: range.from, text: content }; break; } case "Right" /* Right */: { change = { from: range.to, to: range.to, text: content }; break; } } changes.push(change); } const transaction = { changes }; const origin = "DirectionalCopy_" + String(direction); editor.transaction(transaction, origin); } }; // src/DirectionalMove.ts var DirectionalMove = class { constructor(app, plugin) { this.app = app; this.plugin = plugin; } /** * Move the current selection(s) in the specified direction (left or right) */ directionalMove(editor, view, direction) { const selections = editor.listSelections(); const changes = []; for (const selection of selections) { const range = selectionToRange(selection); let additionChange; let deletionChange; switch (direction) { case "Left" /* Left */: { deletionChange = { from: { line: range.from.line, ch: range.from.ch - 1 }, to: range.from, text: "" }; additionChange = { from: range.to, to: range.to, text: editor.getRange(deletionChange.from, deletionChange.to) }; break; } case "Right" /* Right */: { deletionChange = { from: range.to, to: { line: range.to.line, ch: range.to.ch + 1 }, text: "" }; additionChange = { from: range.from, to: range.from, text: editor.getRange(deletionChange.from, deletionChange.to) }; break; } } changes.push(deletionChange, additionChange); } const transaction = { changes }; const origin = "DirectionalMove_" + String(direction); editor.transaction(transaction, origin); } }; // src/ToggleHeading.ts var ToggleHeading = class { constructor(app, plugin) { this.app = app; this.plugin = plugin; } /** * Create a change to set the heading level for a specific line */ setHeading(editor, heading, line) { const headingStr = "#".repeat(heading); const text = editor.getLine(line); const matches = HEADING_REGEX.exec(text); const from = { line, ch: 0 }; const to = { line, ch: matches[1].length + matches[2].length }; const replacementStr = heading === 0 /* NORMAL */ ? "" : headingStr + " "; return { from, to, text: replacementStr }; } /** * Get the heading level of a specific line */ getHeadingOfLine(editor, line) { const text = editor.getLine(line); const matches = HEADING_REGEX.exec(text); return matches[1].length; } /** * Toggle heading for the current selection(s) * If the line already has the specified heading, remove it (set to NORMAL) */ toggleHeading(editor, view, heading) { const selections = editor.listSelections(); const mainSelection = getMainSelection(editor); const mainRange = selectionToRange(mainSelection); const mainHeading = this.getHeadingOfLine(editor, mainRange.from.line); const targetHeading = heading === mainHeading ? 0 /* NORMAL */ : heading; const changes = []; const linesToSet = /* @__PURE__ */ new Set(); for (const selection of selections) { const range = selectionToRange(selection); for (let line = range.from.line; line <= range.to.line; line++) { linesToSet.add(line); } } for (const line of linesToSet) { if (editor.getLine(line) === "") { continue; } changes.push(this.setHeading(editor, targetHeading, line)); } const transaction = { changes }; editor.transaction(transaction, "ToggleHeading" + heading.toString()); } /** * Toggle heading with stripping of inline formatting * This removes bold/italic formatting when toggling headings */ toggleHeadingWithStrip(editor, view, level) { const cursor = editor.getCursor(); const line = editor.getLine(cursor.line); const match = line.match(/^(#+)\s(.*)/); let content = line; let currentLevel = 0; if (match) { currentLevel = match[1].length; content = match[2]; } content = content.replace(/(\*\*|__)(.*?)\1/g, "$2").replace(/(\*|_)(.*?)\1/g, "$2"); editor.setLine( cursor.line, currentLevel === level ? content : "#".repeat(level) + " " + content ); } }; // src/SelectionHelper.ts var SelectionHelper = class { constructor(app, plugin) { this.app = app; this.plugin = plugin; } /** * Select the current line(s) */ selectLine(editor, view) { const selections = editor.listSelections(); const newSelectionRanges = []; for (const selection of selections) { const newSelection = selectionToLine(editor, selection); newSelectionRanges.push(selectionToRange(newSelection)); } const transaction = { selections: newSelectionRanges }; editor.transaction(transaction, "SelectionHelper_Line"); } /** * Select the current word(s) */ selectWord(editor, view) { var _a, _b, _c, _d; const selections = editor.listSelections(); const newSelections = []; for (const selection of selections) { const range = selectionToRange(selection); const wordStart = (_b = (_a = editor.wordAt(range.from)) == null ? void 0 : _a.from) != null ? _b : range.from; const wordEnd = (_d = (_c = editor.wordAt(range.to)) == null ? void 0 : _c.to) != null ? _d : range.from; const newSelection = { from: { line: wordStart.line, ch: wordStart.ch }, to: { line: wordEnd.line, ch: wordEnd.ch } }; newSelections.push(newSelection); } const transaction = { selections: newSelections }; editor.transaction(transaction, "SelectionHelper_Word"); } }; // src/FileHelper.ts var import_obsidian2 = require("obsidian"); var FileHelper = class { constructor(app, plugin) { this.app = app; this.plugin = plugin; } /** * Duplicate the current file */ async duplicateFile(editor, view) { var _a, _b; const activeFile = this.app.workspace.getActiveFile(); if (activeFile === null) { return; } const selections = editor.listSelections(); const parentPath = (_b = (_a = activeFile.parent) == null ? void 0 : _a.path) != null ? _b : ""; const newFilePath = parentPath + "/" + activeFile.basename + " (copy)." + activeFile.extension; try { const newFile = await this.app.vault.copy(activeFile, newFilePath); if (view instanceof import_obsidian2.MarkdownView) { await view.leaf.openFile(newFile); } editor.setSelections(selections); } catch (e) { new import_obsidian2.Notice(String(e)); } } /** * Create a new file in the same directory as the current file */ async newAdjacentFile(editor, view) { var _a, _b; const activeFile = this.app.workspace.getActiveFile(); if (activeFile === null) { return; } const parentPath = (_b = (_a = activeFile.parent) == null ? void 0 : _a.path) != null ? _b : ""; const newFilePath = parentPath + "/Untitled.md"; try { const newFile = await this.app.vault.create(newFilePath, ""); if (view instanceof import_obsidian2.MarkdownView) { await view.leaf.openFile(newFile); } } catch (e) { new import_obsidian2.Notice(String(e)); } } }; // src/SentenceNavigator.ts var SentenceNavigator = class { constructor(app, plugin, regexSource) { this.app = app; this.plugin = plugin; this.sentenceRegex = new RegExp(regexSource, "gm"); } /** * Update the sentence regex pattern */ updateRegex(regexSource) { this.sentenceRegex = new RegExp(regexSource, "gm"); } /** * Get cursor position and current paragraph text */ getCursorAndParagraphText(editor) { const cursorPosition = editor.getCursor(); return { cursorPosition, paragraphText: editor.getLine(cursorPosition.line) }; } /** * Iterate over all sentences in a paragraph */ forEachSentence(paragraphText, callback) { for (const sentence of paragraphText.matchAll(this.sentenceRegex)) { if (callback(sentence)) break; } } /** * Set cursor at the next non-space character */ setCursorAtNextWordCharacter(editor, cursorPosition, paragraphText, direction) { let ch = cursorPosition.ch; if (direction === "start") { while (ch > 0 && paragraphText.charAt(ch - 1) === " ") ch--; } else { while (ch < paragraphText.length && paragraphText.charAt(ch) === " ") ch++; } editor.setCursor({ ch, line: cursorPosition.line }); } /** * Get the previous non-empty line number */ getPrevNonEmptyLine(editor, currentLine) { let prevLine = currentLine - 1; while (prevLine >= 0 && editor.getLine(prevLine).length === 0) prevLine--; return prevLine; } /** * Get the next non-empty line number */ getNextNonEmptyLine(editor, currentLine) { let nextLine = currentLine + 1; while (nextLine < editor.lineCount() && editor.getLine(nextLine).length === 0) nextLine++; return nextLine; } /** * Check if cursor is at the start of a list item */ isAtStartOfListItem(cursorPosition, paragraphText) { return LIST_CHARACTER_REGEX.test(paragraphText.slice(0, cursorPosition.ch)); } /** * Delete text from cursor to sentence boundary */ deleteToBoundary(editor, view, boundary) { let { cursorPosition, paragraphText } = this.getCursorAndParagraphText(editor); const originalCursorPosition = cursorPosition; if (paragraphText.charAt(cursorPosition.ch) === " " || paragraphText.charAt(cursorPosition.ch - 1) === " ") { this.setCursorAtNextWordCharacter(editor, cursorPosition, paragraphText, boundary); ({ cursorPosition, paragraphText } = this.getCursorAndParagraphText(editor)); } this.forEachSentence(paragraphText, (sentence) => { var _a; const idx = (_a = sentence.index) != null ? _a : 0; if (cursorPosition.ch >= idx && cursorPosition.ch <= idx + sentence[0].length) { if (boundary === "start") { const newText = paragraphText.substring(0, idx) + paragraphText.substring(originalCursorPosition.ch); const cutLength = paragraphText.length - newText.length; editor.replaceRange( newText, { line: cursorPosition.line, ch: 0 }, { line: cursorPosition.line, ch: paragraphText.length } ); editor.setCursor({ line: cursorPosition.line, ch: originalCursorPosition.ch - cutLength }); } else { const remainingLength = idx + sentence[0].length - cursorPosition.ch; const newText = paragraphText.substring(0, originalCursorPosition.ch) + paragraphText.substring(cursorPosition.ch + remainingLength); editor.replaceRange( newText, { line: cursorPosition.line, ch: 0 }, { line: cursorPosition.line, ch: paragraphText.length } ); editor.setCursor(originalCursorPosition); } return true; } }); } /** * Select text from cursor to sentence boundary */ selectToBoundary(editor, view, boundary) { const { cursorPosition, paragraphText } = this.getCursorAndParagraphText(editor); this.forEachSentence(paragraphText, (sentence) => { var _a; const idx = (_a = sentence.index) != null ? _a : 0; if (cursorPosition.ch >= idx && cursorPosition.ch <= idx + sentence[0].length) { if (editor.getSelection().length > 0 && (cursorPosition.ch === idx || cursorPosition.ch === idx + sentence[0].length)) { return true; } if (boundary === "start") { const precedingLength = cursorPosition.ch - idx; editor.setSelection(cursorPosition, { line: cursorPosition.line, ch: cursorPosition.ch - precedingLength }); } else { editor.setSelection(cursorPosition, { line: cursorPosition.line, ch: idx + sentence[0].length }); } return true; } }); } /** * Move cursor to the start of the current sentence */ moveToStartOfCurrentSentence(editor, view) { let { cursorPosition, paragraphText } = this.getCursorAndParagraphText(editor); if (cursorPosition.ch === 0 || this.isAtStartOfListItem(cursorPosition, paragraphText)) { const prevLine = this.getPrevNonEmptyLine(editor, cursorPosition.line); if (prevLine >= 0) { editor.setCursor({ line: prevLine, ch: editor.getLine(prevLine).length }); } return; } this.forEachSentence(paragraphText, (sentence) => { var _a; const idx = (_a = sentence.index) != null ? _a : 0; while (cursorPosition.ch > 0 && paragraphText.charAt(cursorPosition.ch - 1) === " ") { editor.setCursor({ line: cursorPosition.line, ch: --cursorPosition.ch }); } if (cursorPosition.ch > idx && idx + sentence[0].length >= cursorPosition.ch) { editor.setCursor({ line: cursorPosition.line, ch: idx }); } }); } /** * Move cursor to the start of the next sentence */ moveToStartOfNextSentence(editor, view) { let { cursorPosition, paragraphText } = this.getCursorAndParagraphText(editor); if (cursorPosition.ch === paragraphText.length) { const nextLine = this.getNextNonEmptyLine(editor, cursorPosition.line); if (nextLine < editor.lineCount()) { editor.setCursor({ line: nextLine, ch: 0 }); } return; } this.forEachSentence(paragraphText, (sentence) => { var _a; const idx = (_a = sentence.index) != null ? _a : 0; while (cursorPosition.ch < paragraphText.length && paragraphText.charAt(cursorPosition.ch) === " ") { editor.setCursor({ line: cursorPosition.line, ch: ++cursorPosition.ch }); } if (cursorPosition.ch >= idx && cursorPosition.ch <= idx + sentence[0].length) { const endPos = { line: cursorPosition.line, ch: idx + sentence[0].length }; this.setCursorAtNextWordCharacter(editor, endPos, paragraphText, "end"); if (endPos.ch >= paragraphText.length) { const nextLine = this.getNextNonEmptyLine(editor, cursorPosition.line); if (nextLine < editor.lineCount()) { editor.setCursor({ line: nextLine, ch: 0 }); } } } }); } /** * Select the current sentence */ selectSentence(editor, view) { const { cursorPosition, paragraphText } = this.getCursorAndParagraphText(editor); let offset = 0; let text = paragraphText; const matches = paragraphText.match(/^(\d+\.|[-*+]) /); if (matches) { offset = matches[0].length; text = paragraphText.slice(offset); } this.forEachSentence(text, (sentence) => { var _a; const idx = (_a = sentence.index) != null ? _a : 0; if (cursorPosition.ch <= offset + idx + sentence[0].length) { editor.setSelection( { line: cursorPosition.line, ch: offset + idx }, { line: cursorPosition.line, ch: offset + idx + sentence[0].length } ); return true; } }); } }; // src/SelectionOccurrence.ts var isManualSelection = true; var isProgrammaticSelectionChange = false; var setIsManualSelection = (value) => { isManualSelection = value; }; var setIsProgrammaticSelectionChange = (value) => { isProgrammaticSelectionChange = value; }; var getIsProgrammaticSelectionChange = () => isProgrammaticSelectionChange; function hasSameSelectionContent(editor, selections) { return new Set( selections.map((selection) => { const { from, to } = selectionToRange(selection); return editor.getRange(from, to); }) ).size === 1; } function getSearchText({ editor, allSelections, autoExpand }) { const singleSearchText = hasSameSelectionContent(editor, allSelections); const firstSelection = allSelections[0]; const { from, to } = selectionToRange(firstSelection); let searchText = editor.getRange(from, to); if (searchText.length === 0 && autoExpand) { const wordRange = editor.wordAt(from); if (wordRange) { searchText = editor.getRange(wordRange.from, wordRange.to); } } return { searchText, singleSearchText }; } var escapeRegex = (input) => input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); var withWordBoundaries = (input) => `(?<=\\W|^)${input}(?=\\W|$)`; function findAllMatches({ searchText, searchWithinWords, documentContent }) { const escapedSearchText = escapeRegex(searchText); const searchExpression = new RegExp( searchWithinWords ? escapedSearchText : withWordBoundaries(escapedSearchText), "g" ); return Array.from(documentContent.matchAll(searchExpression)); } function findAllMatchPositions({ editor, searchText, searchWithinWords, documentContent }) { const matches = findAllMatches({ searchText, searchWithinWords, documentContent }); const matchPositions = []; for (const match of matches) { matchPositions.push({ anchor: editor.offsetToPos(match.index), head: editor.offsetToPos(match.index + searchText.length) }); } return matchPositions; } function findNextMatchPosition({ editor, latestMatchPos, searchText, searchWithinWords, documentContent }) { const latestMatchOffset = editor.posToOffset(latestMatchPos); const matches = findAllMatches({ searchText, searchWithinWords, documentContent }); for (const match of matches) { if (match.index > latestMatchOffset) { return { anchor: editor.offsetToPos(match.index), head: editor.offsetToPos(match.index + searchText.length) }; } } const selectionIndexes = editor.listSelections().map((selection) => { const { from } = selectionToRange(selection); return editor.posToOffset(from); }); for (const match of matches) { if (!selectionIndexes.includes(match.index)) { return { anchor: editor.offsetToPos(match.index), head: editor.offsetToPos(match.index + searchText.length) }; } } return null; } function findPrevMatchPosition({ editor, latestMatchPos, searchText, searchWithinWords, documentContent }) { const latestMatchOffset = editor.posToOffset(latestMatchPos); const matches = findAllMatches({ searchText, searchWithinWords, documentContent }); for (let i = matches.length - 1; i >= 0; i--) { if (matches[i].index < latestMatchOffset) { return { anchor: editor.offsetToPos(matches[i].index), head: editor.offsetToPos(matches[i].index + searchText.length) }; } } const selectionIndexes = editor.listSelections().map((selection) => { const { from } = selectionToRange(selection); return editor.posToOffset(from); }); for (let i = matches.length - 1; i >= 0; i--) { if (!selectionIndexes.includes(matches[i].index)) { return { anchor: editor.offsetToPos(matches[i].index), head: editor.offsetToPos(matches[i].index + searchText.length) }; } } return null; } function selectWordOrNextOccurrence(editor, direction) { setIsProgrammaticSelectionChange(true); const allSelections = editor.listSelections(); const { searchText, singleSearchText } = getSearchText({ editor, allSelections, autoExpand: false }); if (searchText.length > 0 && singleSearchText) { const latestSelection = direction === "next" ? allSelections[allSelections.length - 1] : allSelections[0]; const { from: latestMatchPos } = selectionToRange(latestSelection); const findFn = direction === "next" ? findNextMatchPosition : findPrevMatchPosition; const nextMatch = findFn({ editor, latestMatchPos, searchText, searchWithinWords: isManualSelection, documentContent: editor.getValue() }); const newSelections = nextMatch ? allSelections.concat(nextMatch) : allSelections; editor.setSelections(newSelections); const lastSelection = newSelections[newSelections.length - 1]; editor.scrollIntoView(selectionToRange(lastSelection)); } else { const newSelections = []; for (const selection of allSelections) { const { from, to } = selectionToRange(selection); if (from.line !== to.line || from.ch !== to.ch) { newSelections.push(selection); } else { const wordAt = editor.wordAt(from); if (wordAt) { newSelections.push({ anchor: wordAt.from, head: wordAt.to }); } else { newSelections.push(selection); } setIsManualSelection(false); } } editor.setSelections(newSelections); } } function selectAllOccurrences(editor) { const allSelections = editor.listSelections(); const { searchText, singleSearchText } = getSearchText({ editor, allSelections, autoExpand: true }); if (!singleSearchText) { return; } const matches = findAllMatchPositions({ editor, searchText, searchWithinWords: true, documentContent: editor.getValue() }); editor.setSelections(matches); } // src/main.ts var BindThemPlugin = class extends import_obsidian3.Plugin { async onload() { await this.loadSettings(); this.debug("Loading BindThem..."); this.betterFormatting = new BetterFormatting(this.app, this); this.directionalCopy = new DirectionalCopy(this.app, this); this.directionalMove = new DirectionalMove(this.app, this); this.toggleHeading = new ToggleHeading(this.app, this); this.selectionHelper = new SelectionHelper(this.app, this); this.fileHelper = new FileHelper(this.app, this); this.sentenceNavigator = new SentenceNavigator( this.app, this, this.settings.sentenceRegexSource ); this.registerCommands(); this.registerSelectionChangeListeners(); this.addSettingTab(new BindThemSettingTab(this.app, this)); } onunload() { console.debug("Unloading BindThem..."); } /** * Debug logging */ debug(str) { if (this.settings.debug) { console.debug(DEBUG_HEAD + str); } } /** * Check if a command is enabled in settings */ isCommandEnabled(commandId) { return this.settings.enabledCommands[commandId] !== false; } /** * Register a command only if it's enabled in settings */ addConditionalCommand(options) { if (this.isCommandEnabled(options.id)) { this.addCommand({ id: options.id, name: options.name, icon: options.icon, editorCallback: options.editorCallback, callback: options.callback }); } } /** * Register all commands based on current settings */ registerCommands() { this.addConditionalCommand({ id: "select-line", name: "Select Current Line(s)", icon: "text-select", editorCallback: (editor, view) => { this.selectionHelper.selectLine(editor, view); } }); this.addConditionalCommand({ id: "select-word", name: "Select Current Word(s)", icon: "text-cursor", editorCallback: (editor, view) => { this.selectionHelper.selectWord(editor, view); } }); this.addConditionalCommand({ id: "toggle-bold-underscore", name: "Better Toggle Bold (underscores)", icon: "bold", editorCallback: (editor, view) => { this.betterFormatting.toggleWrapper(editor, view, "__", "__"); } }); this.addConditionalCommand({ id: "toggle-bold-asterisk", name: "Better Toggle Bold (asterisks)", icon: "bold", editorCallback: (editor, view) => { this.betterFormatting.toggleWrapper(editor, view, "**", "**"); } }); this.addConditionalCommand({ id: "toggle-italics-underscore", name: "Better Toggle Italics (underscore)", icon: "italic", editorCallback: (editor, view) => { this.betterFormatting.toggleWrapper(editor, view, "_", "_"); } }); this.addConditionalCommand({ id: "toggle-italics-asterisk", name: "Better Toggle Italics (asterisk)", icon: "italic", editorCallback: (editor, view) => { this.betterFormatting.toggleWrapper(editor, view, "*", "*"); } }); this.addConditionalCommand({ id: "toggle-code", name: "Better Toggle Code", icon: "code", editorCallback: (editor, view) => { this.betterFormatting.toggleWrapper(editor, view, "`", "`"); } }); this.addConditionalCommand({ id: "toggle-comment", name: "Better Toggle Comment", icon: "message-square", editorCallback: (editor, view) => { this.betterFormatting.toggleWrapper(editor, view, "%%", "%%"); } }); this.addConditionalCommand({ id: "toggle-highlight", name: "Better Toggle Highlight", icon: "highlighter", editorCallback: (editor, view) => { this.betterFormatting.toggleWrapper(editor, view, "==", "=="); } }); this.addConditionalCommand({ id: "toggle-strikethrough", name: "Better Toggle Strikethrough", icon: "strikethrough", editorCallback: (editor, view) => { this.betterFormatting.toggleWrapper(editor, view, "~~", "~~"); } }); this.addConditionalCommand({ id: "toggle-math-inline", name: "Better Toggle Math (Inline)", icon: "function-square", editorCallback: (editor, view) => { this.betterFormatting.toggleWrapper(editor, view, "$", "$"); } }); this.addConditionalCommand({ id: "toggle-math-block", name: "Better Toggle Math (Block)", icon: "square-function", editorCallback: (editor, view) => { this.betterFormatting.toggleWrapper(editor, view, "$$", "$$"); } }); this.addConditionalCommand({ id: "copy-up", name: "Copy Current Line(s) Up", icon: "copy", editorCallback: (editor, view) => { this.directionalCopy.directionalCopy(editor, view, "Up" /* Up */); } }); this.addConditionalCommand({ id: "copy-down", name: "Copy Current Line(s) Down", icon: "copy", editorCallback: (editor, view) => { this.directionalCopy.directionalCopy(editor, view, "Down" /* Down */); } }); this.addConditionalCommand({ id: "copy-left", name: "Copy Current Selection(s) Left", icon: "copy", editorCallback: (editor, view) => { this.directionalCopy.directionalCopy(editor, view, "Left" /* Left */); } }); this.addConditionalCommand({ id: "copy-right", name: "Copy Current Selections(s) Right", icon: "copy", editorCallback: (editor, view) => { this.directionalCopy.directionalCopy(editor, view, "Right" /* Right */); } }); this.addConditionalCommand({ id: "move-left", name: "Move Selection Left", icon: "move-left", editorCallback: (editor, view) => { this.directionalMove.directionalMove(editor, view, "Left" /* Left */); } }); this.addConditionalCommand({ id: "move-right", name: "Move Selection Right", icon: "move-right", editorCallback: (editor, view) => { this.directionalMove.directionalMove(editor, view, "Right" /* Right */); } }); this.addConditionalCommand({ id: "toggle-heading-1", name: "Toggle Heading - H1", icon: "heading-1", editorCallback: (editor, view) => { this.toggleHeading.toggleHeading(editor, view, 1 /* H1 */); } }); this.addConditionalCommand({ id: "toggle-heading-2", name: "Toggle Heading - H2", icon: "heading-2", editorCallback: (editor, view) => { this.toggleHeading.toggleHeading(editor, view, 2 /* H2 */); } }); this.addConditionalCommand({ id: "toggle-heading-3", name: "Toggle Heading - H3", icon: "heading-3", editorCallback: (editor, view) => { this.toggleHeading.toggleHeading(editor, view, 3 /* H3 */); } }); this.addConditionalCommand({ id: "toggle-heading-4", name: "Toggle Heading - H4", icon: "heading-4", editorCallback: (editor, view) => { this.toggleHeading.toggleHeading(editor, view, 4 /* H4 */); } }); this.addConditionalCommand({ id: "toggle-heading-5", name: "Toggle Heading - H5", icon: "heading-5", editorCallback: (editor, view) => { this.toggleHeading.toggleHeading(editor, view, 5 /* H5 */); } }); this.addConditionalCommand({ id: "toggle-heading-6", name: "Toggle Heading - H6", icon: "heading-6", editorCallback: (editor, view) => { this.toggleHeading.toggleHeading(editor, view, 6 /* H6 */); } }); this.addConditionalCommand({ id: "toggle-heading-strip-1", name: "Toggle Heading - H1 (strip formatting)", icon: "heading-1", editorCallback: (editor, view) => { this.toggleHeading.toggleHeadingWithStrip(editor, view, 1); } }); this.addConditionalCommand({ id: "toggle-heading-strip-2", name: "Toggle Heading - H2 (strip formatting)", icon: "heading-2", editorCallback: (editor, view) => { this.toggleHeading.toggleHeadingWithStrip(editor, view, 2); } }); this.addConditionalCommand({ id: "toggle-heading-strip-3", name: "Toggle Heading - H3 (strip formatting)", icon: "heading-3", editorCallback: (editor, view) => { this.toggleHeading.toggleHeadingWithStrip(editor, view, 3); } }); this.addConditionalCommand({ id: "toggle-heading-strip-4", name: "Toggle Heading - H4 (strip formatting)", icon: "heading-4", editorCallback: (editor, view) => { this.toggleHeading.toggleHeadingWithStrip(editor, view, 4); } }); this.addConditionalCommand({ id: "toggle-heading-strip-5", name: "Toggle Heading - H5 (strip formatting)", icon: "heading-5", editorCallback: (editor, view) => { this.toggleHeading.toggleHeadingWithStrip(editor, view, 5); } }); this.addConditionalCommand({ id: "toggle-heading-strip-6", name: "Toggle Heading - H6 (strip formatting)", icon: "heading-6", editorCallback: (editor, view) => { this.toggleHeading.toggleHeadingWithStrip(editor, view, 6); } }); this.addConditionalCommand({ id: "delete-to-start-of-sentence", name: "Delete to Start of Sentence", icon: "delete", editorCallback: (editor, view) => { this.sentenceNavigator.deleteToBoundary(editor, view, "start"); } }); this.addConditionalCommand({ id: "delete-to-end-of-sentence", name: "Delete to End of Sentence", icon: "delete", editorCallback: (editor, view) => { this.sentenceNavigator.deleteToBoundary(editor, view, "end"); } }); this.addConditionalCommand({ id: "select-to-start-of-sentence", name: "Select to Start of Sentence", icon: "text-select", editorCallback: (editor, view) => { this.sentenceNavigator.selectToBoundary(editor, view, "start"); } }); this.addConditionalCommand({ id: "select-to-end-of-sentence", name: "Select to End of Sentence", icon: "text-select", editorCallback: (editor, view) => { this.sentenceNavigator.selectToBoundary(editor, view, "end"); } }); this.addConditionalCommand({ id: "move-to-start-of-current-sentence", name: "Move to Start of Current Sentence", icon: "arrow-up-left", editorCallback: (editor, view) => { this.sentenceNavigator.moveToStartOfCurrentSentence(editor, view); } }); this.addConditionalCommand({ id: "move-to-start-of-next-sentence", name: "Move to Start of Next Sentence", icon: "arrow-down-right", editorCallback: (editor, view) => { this.sentenceNavigator.moveToStartOfNextSentence(editor, view); } }); this.addConditionalCommand({ id: "select-sentence", name: "Select Current Sentence", icon: "text-quote", editorCallback: (editor, view) => { this.sentenceNavigator.selectSentence(editor, view); } }); this.addConditionalCommand({ id: "duplicate-file", name: "Duplicate File", icon: "copy", editorCallback: (editor, view) => { void this.fileHelper.duplicateFile(editor, view); } }); this.addConditionalCommand({ id: "new-adjacent-file", name: "New Adjacent File", icon: "file-plus", editorCallback: (editor, view) => { void this.fileHelper.newAdjacentFile(editor, view); } }); this.addConditionalCommand({ id: "insert-line-above", name: "Insert Line Above", icon: "plus", editorCallback: (editor, view) => { this.insertLine(editor, view, "above"); } }); this.addConditionalCommand({ id: "insert-line-below", name: "Insert Line Below", icon: "plus", editorCallback: (editor, view) => { this.insertLine(editor, view, "below"); } }); this.addConditionalCommand({ id: "delete-line", name: "Delete Line", icon: "trash", editorCallback: (editor, view) => { this.deleteLine(editor); } }); this.addConditionalCommand({ id: "delete-to-start-of-line", name: "Delete to Start of Line", icon: "delete", editorCallback: (editor, view) => { this.deleteToLineBoundary(editor, "start"); } }); this.addConditionalCommand({ id: "delete-to-end-of-line", name: "Delete to End of Line", icon: "delete", editorCallback: (editor, view) => { this.deleteToLineBoundary(editor, "end"); } }); this.addConditionalCommand({ id: "join-lines", name: "Join Lines", icon: "merge", editorCallback: (editor, view) => { this.joinLines(editor); } }); this.addConditionalCommand({ id: "duplicate-line", name: "Duplicate Line", icon: "copy", editorCallback: (editor, view) => { this.copyLine(editor, "down"); } }); this.addConditionalCommand({ id: "copy-line-up", name: "Copy Line Up", icon: "copy", editorCallback: (editor, view) => { this.copyLine(editor, "up"); } }); this.addConditionalCommand({ id: "copy-line-down", name: "Copy Line Down", icon: "copy", editorCallback: (editor, view) => { this.copyLine(editor, "down"); } }); this.addConditionalCommand({ id: "transform-uppercase", name: "Transform to Uppercase", icon: "arrow-up", editorCallback: (editor, view) => { this.transformCase(editor, 0 /* Upper */); } }); this.addConditionalCommand({ id: "transform-lowercase", name: "Transform to Lowercase", icon: "arrow-down", editorCallback: (editor, view) => { this.transformCase(editor, 1 /* Lower */); } }); this.addConditionalCommand({ id: "transform-titlecase", name: "Transform to Title Case", icon: "heading", editorCallback: (editor, view) => { this.transformCase(editor, 2 /* Title */); } }); this.addConditionalCommand({ id: "toggle-case", name: "Toggle Case", icon: "case-sensitive", editorCallback: (editor, view) => { this.transformCase(editor, 3 /* Next */); } }); this.addConditionalCommand({ id: "go-to-line-start", name: "Go to Line Start", icon: "arrow-left", editorCallback: (editor, view) => { editor.setCursor({ line: editor.getCursor().line, ch: 0 }); } }); this.addConditionalCommand({ id: "go-to-line-end", name: "Go to Line End", icon: "arrow-right", editorCallback: (editor) => { editor.setCursor({ line: editor.getCursor().line, ch: editor.getLine(editor.getCursor().line).length }); } }); this.addConditionalCommand({ id: "go-to-first-line", name: "Go to First Line", icon: "arrow-up-from-line", editorCallback: (editor) => { editor.setCursor({ line: 0, ch: 0 }); } }); this.addConditionalCommand({ id: "go-to-last-line", name: "Go to Last Line", icon: "arrow-down-to-line", editorCallback: (editor) => { editor.setCursor({ line: editor.lineCount() - 1, ch: 0 }); } }); this.addConditionalCommand({ id: "go-to-line-number", name: "Go to Line Number", icon: "text", editorCallback: (editor) => { this.showGoToLineModal(editor); } }); this.addConditionalCommand({ id: "go-to-next-heading", name: "Go to Next Heading", icon: "arrow-down", editorCallback: (editor) => { this.goToHeading(editor, "next"); } }); this.addConditionalCommand({ id: "go-to-prev-heading", name: "Go to Previous Heading", icon: "arrow-up", editorCallback: (editor) => { this.goToHeading(editor, "prev"); } }); this.addConditionalCommand({ id: "move-cursor-up", name: "Move Cursor Up", icon: "arrow-up", editorCallback: (editor) => { editor.exec("goUp"); } }); this.addConditionalCommand({ id: "move-cursor-down", name: "Move Cursor Down", icon: "arrow-down", editorCallback: (editor) => { editor.exec("goDown"); } }); this.addConditionalCommand({ id: "move-cursor-left", name: "Move Cursor Left", icon: "arrow-left", editorCallback: (editor) => { editor.exec("goLeft"); } }); this.addConditionalCommand({ id: "move-cursor-right", name: "Move Cursor Right", icon: "arrow-right", editorCallback: (editor) => { editor.exec("goRight"); } }); this.addConditionalCommand({ id: "go-to-prev-word", name: "Go to Previous Word", icon: "arrow-left", editorCallback: (editor) => { editor.exec("goWordLeft"); } }); this.addConditionalCommand({ id: "go-to-next-word", name: "Go to Next Word", icon: "arrow-right", editorCallback: (editor) => { editor.exec("goWordRight"); } }); this.addConditionalCommand({ id: "insert-cursor-above", name: "Insert Cursor Above", icon: "plus-circle", editorCallback: (editor) => { this.insertCursor(editor, -1); } }); this.addConditionalCommand({ id: "insert-cursor-below", name: "Insert Cursor Below", icon: "plus-circle", editorCallback: (editor) => { this.insertCursor(editor, 1); } }); this.addConditionalCommand({ id: "select-next-occurrence", name: "Select Next Occurrence", icon: "text-cursor", editorCallback: (editor) => { selectWordOrNextOccurrence(editor, "next"); } }); this.addConditionalCommand({ id: "select-prev-occurrence", name: "Select Previous Occurrence", icon: "text-cursor", editorCallback: (editor) => { selectWordOrNextOccurrence(editor, "prev"); } }); this.addConditionalCommand({ id: "select-all-occurrences", name: "Select All Occurrences", icon: "text-cursor-input", editorCallback: (editor) => { selectAllOccurrences(editor); } }); this.addConditionalCommand({ id: "toggle-line-numbers", name: "Toggle Line Numbers", icon: "list", callback: () => { const vaultWithConfig = this.app.vault; const currentValue = vaultWithConfig.getConfig("showLineNumber"); vaultWithConfig.setConfig("showLineNumber", !currentValue); } }); this.addConditionalCommand({ id: "undo", name: "Undo", icon: "undo", editorCallback: (editor) => { editor.undo(); } }); this.addConditionalCommand({ id: "redo", name: "Redo", icon: "redo", editorCallback: (editor) => { editor.redo(); } }); } // ============================================================ // Private Helper Methods // ============================================================ insertLine(editor, view, where) { const cursor = editor.getCursor(); const indent = getLeadingWhitespace(editor.getLine(cursor.line)); if (where === "above") { editor.replaceRange(indent + "\n", { line: cursor.line, ch: 0 }); editor.setCursor({ line: cursor.line, ch: indent.length }); } else { editor.replaceRange( "\n" + indent, { line: cursor.line, ch: editor.getLine(cursor.line).length } ); editor.setCursor({ line: cursor.line + 1, ch: indent.length }); } } deleteLine(editor) { const cursor = editor.getCursor(); const isLastLine = cursor.line === editor.lineCount() - 1; editor.replaceRange( "", { line: cursor.line, ch: 0 }, isLastLine ? { line: cursor.line, ch: editor.getLine(cursor.line).length } : { line: cursor.line + 1, ch: 0 } ); } deleteToLineBoundary(editor, boundary) { const cursor = editor.getCursor(); editor.replaceRange( "", boundary === "start" ? { line: cursor.line, ch: 0 } : cursor, boundary === "start" ? cursor : { line: cursor.line, ch: editor.getLine(cursor.line).length } ); } joinLines(editor) { const cursor = editor.getCursor(); if (cursor.line >= editor.lineCount() - 1) return; const curr = editor.getLine(cursor.line); const next = editor.getLine(cursor.line + 1).replace(/^\s*[-+*>\d.]+\s*/, ""); editor.replaceRange( (curr.endsWith(" ") || next.startsWith(" ") ? "" : " ") + next, { line: cursor.line, ch: curr.length }, { line: cursor.line + 1, ch: editor.getLine(cursor.line + 1).length } ); } copyLine(editor, direction) { const cursor = editor.getCursor(); const line = editor.getLine(cursor.line); if (direction === "up") { editor.replaceRange(line + "\n", { line: cursor.line, ch: 0 }); } else { editor.replaceRange("\n" + line, { line: cursor.line, ch: line.length }); editor.setCursor({ line: cursor.line + 1, ch: cursor.ch }); } } transformCase(editor, caseType) { const sel = editor.getSelection(); const cursor = editor.getCursor(); const lineContent = editor.getLine(cursor.line); const wordRange = wordRangeAtPos(cursor, lineContent); const text = sel || editor.getRange(wordRange.anchor, wordRange.head); let result; switch (caseType) { case 0 /* Upper */: result = text.toUpperCase(); break; case 1 /* Lower */: result = text.toLowerCase(); break; case 2 /* Title */: result = toTitleCase(text); break; case 3 /* Next */: result = text === text.toUpperCase() ? text.toLowerCase() : text === text.toLowerCase() ? toTitleCase(text) : text.toUpperCase(); break; } if (sel) { editor.replaceSelection(result); } else { editor.replaceRange(result, wordRange.anchor, wordRange.head); } } insertCursor(editor, offset) { const newSels = []; for (const sel of editor.listSelections()) { const newLine = sel.head.line + offset; if (newLine >= 0 && newLine < editor.lineCount()) { newSels.push({ anchor: { line: sel.anchor.line + offset, ch: Math.min(sel.anchor.ch, editor.getLine(newLine).length) }, head: { line: newLine, ch: Math.min(sel.head.ch, editor.getLine(newLine).length) } }); } } editor.setSelections([...editor.listSelections(), ...newSels]); } goToHeading(editor, direction) { var _a; const file = this.app.workspace.getActiveFile(); const cache = file ? this.app.metadataCache.getFileCache(file) : null; if (!((_a = cache == null ? void 0 : cache.headings) == null ? void 0 : _a.length)) return; const cursorLine = editor.getCursor().line; let target = direction === "next" ? editor.lineCount() - 1 : 0; for (const h of cache.headings) { const line = h.position.end.line; if (direction === "next" && line > cursorLine && line < target) target = line; if (direction === "prev" && line < cursorLine && line > target) target = line; } editor.setCursor({ line: target, ch: 0 }); editor.scrollIntoView({ from: { line: target, ch: 0 }, to: { line: target, ch: 0 } }); } showGoToLineModal(editor) { const lineCount = editor.lineCount(); const overlay = document.createElement("div"); overlay.className = "modal-bg"; overlay.setCssStyles({ position: "fixed", top: "0", left: "0", width: "100%", height: "100%", background: "rgba(0,0,0,0.5)", zIndex: "1000" }); const modalEl = document.createElement("div"); modalEl.className = "modal-container bindthem-modal"; modalEl.setCssStyles({ position: "fixed", top: "50%", left: "50%", transform: "translate(-50%, -50%)", background: "var(--background-primary)", padding: "20px", borderRadius: "8px", zIndex: "1001", maxWidth: "300px" }); const title = document.createElement("h2"); title.textContent = "Go to line"; title.setCssStyles({ marginTop: "0" }); modalEl.appendChild(title); const inputEl = document.createElement("input"); inputEl.type = "number"; inputEl.min = "1"; inputEl.max = String(lineCount); inputEl.setCssStyles({ width: "100%", marginBottom: "10px", padding: "8px" }); modalEl.appendChild(inputEl); const btn = document.createElement("button"); btn.textContent = "Go"; btn.setCssStyles({ padding: "8px 16px" }); btn.onclick = () => { const n = parseInt(inputEl.value) - 1; if (n >= 0 && n < lineCount) { editor.setCursor({ line: n, ch: 0 }); overlay.remove(); } }; modalEl.appendChild(btn); inputEl.onkeydown = (e) => { if (e.key === "Enter") btn.click(); if (e.key === "Escape") overlay.remove(); }; overlay.appendChild(modalEl); document.body.appendChild(overlay); inputEl.focus(); } // ============================================================ // Selection Change Listeners // ============================================================ /** * Registers event listeners on CodeMirror editors to track whether a * selection change was manual (mouse/keyboard) or programmatic. * This controls the match behavior for select next/previous occurrence: * - Manual changes → whole-word matching * - Programmatic changes → within-word matching */ registerSelectionChangeListeners() { this.app.workspace.onLayoutReady(() => { const MODIFIER_KEYS = [ "Control", "Shift", "Alt", "Meta", "CapsLock", "Fn" ]; const handleSelectionChange = (evt) => { if (evt instanceof KeyboardEvent && MODIFIER_KEYS.includes(evt.key)) { return; } if (!getIsProgrammaticSelectionChange()) { setIsManualSelection(true); } setIsProgrammaticSelectionChange(false); }; document.querySelectorAll(".cm-content").forEach((el) => { this.registerDomEvent(el, "keydown", handleSelectionChange); this.registerDomEvent(el, "click", handleSelectionChange); this.registerDomEvent(el, "dblclick", handleSelectionChange); }); }); } // ============================================================ // Settings // ============================================================ async loadSettings() { const savedData = await this.loadData(); const defaultCommands = createDefaultEnabledCommands(); this.settings = { ...DEFAULT_SETTINGS, ...savedData, enabledCommands: { ...defaultCommands, ...savedData == null ? void 0 : savedData.enabledCommands } }; } async saveSettings() { await this.saveData(this.settings); this.sentenceNavigator.updateRegex(this.settings.sentenceRegexSource); } };