Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable visual snippets for vim #359

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ To create a visual snippet, you can alternatively use the `v` option and make th
.

Visual snippets will not expand unless text is selected.
You can also trigger visual snippets with vim, see [select-mode](#select-mode)


### Function snippets
Expand Down Expand Up @@ -280,3 +281,26 @@ You can [view snippets written by others and share your own snippets here](https
> [!WARNING]
> Snippet files are interpreted as JavaScript and can execute arbitrary code.
> Always be careful with snippets shared from others to avoid running malicious code.


## Vim

[Vim](https://vimhelp.org/intro.txt.html#intro.txt) is a powerful and highly configurable text editor, that is known for its unique editing style. It allows you to perform complex editing tasks with minimal keystrokes. It is heavily centered around moving with the keys `hjkl` instead of using the mouse. This can make the tedious parts of editing a bit less tedious.

But this intro can't do it justice, so for those unfamiliar that are interested, it's recommended to read [Vims](https://vimhelp.org/usr_02.txt.html#usr_02.txt) for a proper explanation.

Obsidian uses a subset of vim's keybindings maintained by [this repo](https://github.com/replit/codemirror-vim). This subset does not cover everything like [select-mode](https://vimhelp.org/visual.txt.html#Select-mode) and has its own bugs. For any issues that may arise, please first check if the problem lies with [CodeMirror-Vim](https://github.com/replit/codemirror-vim) at [vim-playground](https://raw.githack.com/replit/codemirror-vim/master/dev/web-demo.html) or Obsidian before reporting it here.

### Select mode
You can trigger visual snippets in insert mode, but not in visual mode.
Because of this, a shortcut in the vim extension is defined that allows you to switch between insert mode and visual mode while keeping the selection. This should also work with multicursor.

You can define this shortcut in the settings by enabling vim mode in obsidian and in this plugin.
The shortcut should be a vim keybinding with no spaces. Some keybindings are not supported by the original vim extension. Due to this, please first see if the shortcut works for other keys, such as `:imap <C-a> <Esc>` or `:vmap <A-h> 0`. It is known that the shift key (`:map <S-a> <Esc>`) does not work.

Keep in mind that the shortcut from visual -> select mode only works in visual mode, and the shortcut from select -> visual mode only works when something is selected and you are in insert mode.

Macros are not yet supported for visual snippets.

### Vim matrix shortcut \\\\
Same key mapping rules apply. You can define or redefine `open a new line below` action, which also inserts \\\\ at the end of the current line if it's in a matrix environment.
114 changes: 113 additions & 1 deletion src/features/editor_commands.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { Editor } from "obsidian";
import { Editor, EditorSelection } from "obsidian";
import { EditorView } from "@codemirror/view";
import { replaceRange, setCursor, setSelection } from "../utils/editor_utils";
import LatexSuitePlugin from "src/main";
import { Context } from "src/utils/context";
import { CodeMirrorEditor, Vim } from "src/utils/vim_types";
import { LatexSuitePluginSettings } from "src/settings/settings";
import { runMatrixShortcuts } from "./matrix_shortcuts";
import { insertNewlineAndIndent } from "@codemirror/commands";
import { Transaction, Annotation, TransactionSpec } from "@codemirror/state";


function boxCurrentEquation(view: EditorView) {
Expand Down Expand Up @@ -125,3 +130,110 @@ export const getEditorCommands = (plugin: LatexSuitePlugin) => {
getDisableAllFeaturesCommand(plugin)
];
};

export interface vimCommand {
id: string;
defineType: "defineMotion" | "defineOperator" | "defineAction";
type: "action" | "operator" | "motion";
action: (cm: CodeMirrorEditor) => void;
key: string;
context?: "normal" | "visual" | "replace" | "insert";
}

export function getVimSelectModeCommand(settings: LatexSuitePluginSettings): vimCommand {
return {
id: "latex-suite-vim-select-mode",
defineType: "defineAction",
type: "action",
// copies current selection and selects it again since changing vim modes deletes the selection
action: (cm: CodeMirrorEditor) => {
//@ts-ignore undocumented object
const vimObject: Vim | null = window?.CodeMirrorAdapter?.Vim;
if (!vimObject) return;
const selection: EditorSelection[] = cm.listSelections();
vimObject.enterInsertMode(cm);
cm.setSelections(selection);
},
key: settings.vimSelectMode,
context: "visual",
}
}

export function getVimVisualModeCommand(settings: LatexSuitePluginSettings): vimCommand {
return {
id: "latex-suite-vim-visual-mode",
defineType: "defineAction",
type: "action",
// copies current selection and selects it again since changing vim modes deletes the selection
action: (cm: CodeMirrorEditor) => {
if (!cm.somethingSelected()) return;
const selection: EditorSelection[] = cm.listSelections();
//@ts-ignore undocumented object
const vimObject: Vim | null = window?.CodeMirrorAdapter?.Vim;
if (!vimObject) return;
vimObject.exitInsertMode(cm);
cm.setSelections(selection);
},
key: settings.vimVisualMode,
context: "insert",
}
}

export function getVimRunMatrixEnterCommand(settings: LatexSuitePluginSettings): vimCommand {
return {
id: "latex-suite-vim-special-enter",
defineType: "defineAction",
type: "action",
action: (cm: CodeMirrorEditor) => {
//@ts-ignore
const vimObj: Vim | null = window?.CodeMirrorAdapter?.Vim;
if (!vimObj) return;
const cursorLine: number = cm.getCursor().line;
const line: string = cm.getLine(cursorLine);
cm.setCursor({line: cursorLine, ch: line.length + 1})
const view = EditorView.findFromDOM(cm.getWrapperElement());
const ctx = Context.fromView(view);
if (runMatrixShortcuts(view, ctx, "Enter", false)) {
vimObj.enterInsertMode(cm);
return;
}
// code taken from vim plugin, documentation is not clear on what this does
const succes: boolean = insertNewlineAndIndent({
state: cm.cm6.state,
dispatch: (transaction: Transaction & TransactionSpec) => {
const view: EditorView = cm.cm6;
// should not fire by the design of insertNewlineAndIndent but its in the vim plugin so it is included
if (view.state.readOnly) return;
let type: string = "input.type.compose";
if (cm.curOp && !cm.curOp.lastChange) type = "input.type.compose.start";
if (Array.isArray(transaction.annotations)) {
try {
transaction.annotations.forEach((note: Annotation<string|number|boolean>) => {
//@ts-ignore its "supposed" to be readonly but it is not
if (note.value === "input") note.value = type;
});
} catch (e) {
console.error(e);
}
} else {
transaction.userEvent = type;
}
view.dispatch(transaction);
}
});
if (!succes) console.error(`Failed to insert newline and indent latex-suite-insert-newline-and-indent`);
// go into insert mode after the newline is inserted, otherwise macros rerun for some reason
vimObj.enterInsertMode(cm);
},
key: settings.vimMatrixEnter,
context: "normal",
}
}

export function getVimEditorCommands(settings: LatexSuitePluginSettings): vimCommand[] {
return [
getVimSelectModeCommand(settings),
getVimVisualModeCommand(settings),
getVimRunMatrixEnterCommand(settings),
]
}
17 changes: 16 additions & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { LatexSuitePluginSettings, DEFAULT_SETTINGS, LatexSuiteCMSettings, proce
import { LatexSuiteSettingTab } from "./settings/settings_tab";
import { ICONS } from "./settings/ui/icons";

import { getEditorCommands } from "./features/editor_commands";
import { getEditorCommands, getVimEditorCommands } from "./features/editor_commands";
import { getLatexSuiteConfigExtension } from "./snippets/codemirror/config";
import { SnippetVariables, parseSnippetVariables, parseSnippets } from "./snippets/parse";
import { handleUpdate, onKeydown } from "./latex_suite";
Expand All @@ -14,6 +14,7 @@ import { snippetExtensions } from "./snippets/codemirror/extensions";
import { mkConcealPlugin } from "./editor_extensions/conceal";
import { colorPairedBracketsPluginLowestPrec, highlightCursorBracketsPlugin } from "./editor_extensions/highlight_brackets";
import { cursorTooltipBaseTheme, cursorTooltipField } from "./editor_extensions/math_tooltip";
import { Vim } from "./utils/vim_types";

export default class LatexSuitePlugin extends Plugin {
settings: LatexSuitePluginSettings;
Expand Down Expand Up @@ -204,6 +205,20 @@ export default class LatexSuitePlugin extends Plugin {
for (const command of getEditorCommands(this)) {
this.addCommand(command);
}
vimcommand: {
//check if vim is enabled and accessible
//@ts-ignore
if (!app?.isVimEnabled()) break vimcommand;
if (!this.settings.vimEnabled) break vimcommand;
//@ts-ignore undocumented object
const vimObject: Vim | null = window?.CodeMirrorAdapter?.Vim;
if (!vimObject) break vimcommand;
for (const command of getVimEditorCommands(this.settings)) {
vimObject[command.defineType](command.id, command.action);
vimObject.mapCommand(command.key, command.type, command.id, {}, { context: command.context });
}
}

}

watchFiles() {
Expand Down
9 changes: 9 additions & 0 deletions src/settings/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ interface LatexSuiteBasicSettings {
taboutEnabled: boolean;
autoEnlargeBrackets: boolean;
wordDelimiters: string;
vimEnabled: boolean;
vimSelectMode: string;
vimVisualMode: string;
vimMatrixEnter: string;

}

/**
Expand Down Expand Up @@ -85,6 +90,10 @@ export const DEFAULT_SETTINGS: LatexSuitePluginSettings = {
matrixShortcutsEnvNames: "pmatrix, cases, align, gather, bmatrix, Bmatrix, vmatrix, Vmatrix, array, matrix",
autoEnlargeBracketsTriggers: "sum, int, frac, prod, bigcup, bigcap",
forceMathLanguages: "math",
vimEnabled: false,
vimSelectMode: "<C-g>",
vimVisualMode: "<C-g>",
vimMatrixEnter: "o",
}

export function processLatexSuiteSettings(snippets: Snippet[], settings: LatexSuitePluginSettings):LatexSuiteCMSettings {
Expand Down
97 changes: 97 additions & 0 deletions src/settings/settings_tab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import LatexSuitePlugin from "../main";
import { DEFAULT_SETTINGS } from "./settings";
import { FileSuggest } from "./ui/file_suggest";
import { basicSetup } from "./ui/snippets_editor/extensions";
import { getVimSelectModeCommand, vimCommand, getVimVisualModeCommand, getVimEditorCommands, getVimRunMatrixEnterCommand } from "src/features/editor_commands";
import { Vim } from "src/utils/vim_types";


export class LatexSuiteSettingTab extends PluginSettingTab {
Expand Down Expand Up @@ -492,6 +494,101 @@ export class LatexSuiteSettingTab extends PluginSettingTab {

await this.plugin.saveSettings();
}));
//The toggle both hides the settings and makes the plugin not load the vim commands on startup.
// the vim toggle is loaded before the rest since expanding down looks better.
const vimEnabled: Setting = new Setting(containerEl)
.setName("Vim key bindings")
.setDesc("turn on/off vim keybindings. Note vim needs to be enabled in obsidian itself.")
const vimSettings: Setting[] = [];
const selectMode: Setting = new Setting(containerEl)
.setName("Vim: Switch from visual mode to select mode")
.setDesc(`maps the key to switch from visual mode to select mode.
Keymap must be a vim keymap and can't contain any spaces. Use empty string to disable this feature.
(select mode=insert keybindings)`)
.addText((text) =>
text.setPlaceholder(DEFAULT_SETTINGS.vimSelectMode)
.setValue(this.plugin.settings.vimSelectMode)
.onChange(async (value) => {
const oldValue: string = this.plugin.settings.vimSelectMode;
this.plugin.settings.vimSelectMode = value;
await this.plugin.saveSettings();
//@ts-ignore undocumented object
const vimObject: Vim | null = window?.CodeMirrorAdapter?.Vim;
if (!vimObject) return;
const command: vimCommand = getVimSelectModeCommand(this.plugin.settings);
vimObject[command.defineType](command.id, command.action);
vimObject.mapCommand(command.key, command.type, command.id, {}, { context: command.context });
// this unmaps the current keybinding and reverts to the previous keyMap. For example if oldValue = "<C-c>", then "<C-c>" goes back to "enter normal mode" in default settings.
vimObject.unmap(oldValue, command.context);
})
);
vimSettings.push(selectMode);
const visualMode = new Setting(containerEl)
.setName("Vim: Switch from select mode to visual mode")
.setDesc(`maps the key to switch from select mode to visual mode.
must be a vim keymap and can't contain any spaces. Example <C-g><C-A-i> = Ctrl-g + Ctrl-Alt-i.
Please check the vim keybinding first on another command like w before reporting it. Some keybindings like shift don't work due to the original vim plugin. Use empty string to disable this feature.
(select mode=insert keybindings)`)
.addText((text) =>{
text.setPlaceholder(DEFAULT_SETTINGS.vimVisualMode)
.setValue(this.plugin.settings.vimVisualMode)
.onChange(async (value) => {
const oldvalue: string = this.plugin.settings.vimVisualMode;
this.plugin.settings.vimVisualMode = value;
await this.plugin.saveSettings();
const command: vimCommand = getVimVisualModeCommand(this.plugin.settings);
//@ts-ignore undocumented object
const vimObject: Vim | null = window?.CodeMirrorAdapter?.Vim;
if (!vimObject) return;
vimObject[command.defineType](command.id, command.action);
vimObject.mapCommand(command.key, command.type, command.id, {}, { context: command.context });
// this unmaps the current keybinding and reverts to the previous keyMap. For example if oldValue = "<C-c>", then "<C-c>" goes back to "enter normal mode" in default settings.
vimObject.unmap(oldvalue, command.context);
})
});
vimSettings.push(visualMode);
const matrixEnter: Setting = new Setting(containerEl)
.setName("Vim: run matrix enter")
.setDesc(`maps the key to the action of inserting a new line below while appending \\\\ to the current line in a matrix environment.
Use empty string to disable this feature.`)
.addText((text) => {
text.setPlaceholder(DEFAULT_SETTINGS.vimMatrixEnter)
.setValue(this.plugin.settings.vimMatrixEnter)
.onChange(async (value) => {
const oldValue: string = this.plugin.settings.vimMatrixEnter;
this.plugin.settings.vimMatrixEnter = value;
await this.plugin.saveSettings();
//@ts-ignore
const vimObj: Vim | null = window?.CodeMirrorAdapter?.Vim;
if (!vimObj) return;
vimObj.unmap(oldValue, "normal");
const command: vimCommand = getVimRunMatrixEnterCommand(this.plugin.settings);
vimObj[command.defineType](command.id, command.action);
vimObj.mapCommand(command.key, command.type, command.id, {}, { context: command.context });
vimObj.unmap(oldValue, command.context)
})
});
vimSettings.push(matrixEnter);
// shows/hides the vim settings, since these settings are not needed if vim is not enabled.
vimEnabled.addToggle((toggle) => {
//@ts-ignore
const vimOn: boolean = this.plugin.settings.vimEnabled && app?.isVimEnabled();
vimSettings.forEach(setting => setting.settingEl.toggleClass("hidden", !vimOn));
toggle
//@ts-ignore app.isVimEnabled() is not documented
.setValue(this.plugin.settings.vimEnabled && app?.isVimEnabled())
.onChange(async (value) => {
this.plugin.settings.vimEnabled = value;
await this.plugin.saveSettings();
vimSettings.forEach(setting => setting.settingEl.toggleClass("hidden", !value));
//@ts-ignore undocumented object
const vimObject: Vim | null = window?.CodeMirrorAdapter?.Vim;
if (!vimObject) return;
for (const command of getVimEditorCommands(this.plugin.settings)) {
vimObject.unmap(command.key, command.context);
}
});
});
}

createSnippetsEditor(snippetsSetting: Setting) {
Expand Down
Loading