Android vim mode: Modal dialog alternative for broken command line

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 :noh command))

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');
}
1 Like