Many Android users experience a problem where calling in Vim-mode the command (:) or search (/, ?) menu from the soft keyboard causes the command line to appear and immediately disappear:
I really love Vim mode in Obsidian. It has made my work on Android incredibly faster. The Vimrc Support plugin is also a must-have - I have many configs, use all of them, and periodically add and modify them.
I really wanted to see how the vim command line works and what benefits it can provide.
To solve this, I wrote a startup script for CodeScript Toolkit. The script bypasses the Android IME bug by providing a working interface for entering vim commands through a stable modal dialog, while using Obsidian’s built-in vim handler to execute commands.
To use it, add to your .obsidian.vimrc file:
" Free up Space for binding
unmap <Space>
" Mapping for modal Ex-command input
exmap vimExModal obcommand vim-ex-command-modal
nmap <Space>: :vimExModal<CR>
Now in vim normal mode, when you press <Space>:, a modal dialog will open for command input (enter commands without the leading :).
I should note that I didn’t attempt to implement the search command line (?, /) in the script, as it started to significantly complicate the logic.
Everything seems to work fine; however, I don’t understand what advantages this provides:
- To use it, you need to enter the full command. It’s easier to just assign a mapping in
.obsidian.vimrc - As I understand it, the command line is quite limited in functionality in CodeMirror
So far, the only thing I’ve found it useful for is:
- Removing search highlighting with the
:nohcommand))
I’ve left it as-is for now. I would appreciate comments that would improve my understanding of how to properly use this in the mobile version of Obsidian. Maybe it can be refined to make it a more useful tool.
Here is the code:
import { App, Modal, Setting, Notice } from 'obsidian';
class VimCommandModal extends Modal {
private command: string = '';
private onSubmit: (command: string) => void;
constructor(app: App, onSubmit: (command: string) => void) {
super(app);
this.onSubmit = onSubmit;
}
onOpen() {
const { contentEl } = this;
contentEl.createEl('h3', { text: 'Vim Ex Command' });
new Setting(contentEl)
.setName('Command')
.addText((text) => {
text.setValue('')
.onChange((value) => {
this.command = value;
})
.inputEl.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
this.submit();
} else if (e.key === 'Escape') {
this.close();
}
});
setTimeout(() => text.inputEl.focus(), 10);
});
new Setting(contentEl)
.addButton((btn) =>
btn
.setButtonText('Execute')
.setCta()
.onClick(() => this.submit())
)
.addButton((btn) =>
btn
.setButtonText('Cancel')
.onClick(() => this.close())
);
}
submit() {
this.close();
this.onSubmit(this.command.trim());
}
onClose() {
const { contentEl } = this;
contentEl.empty();
}
}
function executeVimCommand(app: App, command: string) {
if (!command) return;
const activeView = app.workspace.getActiveViewOfType(require('obsidian').MarkdownView);
if (!activeView) return;
const editor = activeView.editor;
const cm = (editor as any).cm?.cm;
if (!cm) return;
const Vim = cm.constructor?.Vim;
if (!Vim) return;
try {
Vim.handleEx(cm, command);
} catch (e) {
console.error('Vim command error:', e);
new Notice('Command failed: ' + command);
}
}
export async function invoke(app: App): Promise<void> {
await new Promise(resolve => setTimeout(resolve, 1000));
// Registering only the Ex command
(app as any).commands.addCommand({
id: 'vim-ex-command-modal',
name: 'Vim: Open Ex Command Modal',
callback: () => {
new VimCommandModal(app, (command) => {
executeVimCommand(app, command);
}).open();
}
});
console.log('Vim Ex command modal registered');
}