Mode hooks and user modes

Kakoune comes with a spell-checking script (spell.kak) but as is convention, it just defines a few commands and leaves the user to figure out how to incorporate them. It occurs to me that what I really want is:

  • declare a custom spell-checking mode
  • add spell-checking-related mappings in that mode
  • execute :spell<ret> on entering that mode
  • execute :spell-clear<ret> on exiting that mode

The first two items in the list are easy enough:

declare-user-mode spell
map global spell n ': spell-next' -docstring "next misspelling"

For the last two items, though, I’m a bit stuck. I clearly can’t use the InsertBegin/InsertEnd/NormalBegin/NormalEnd hooks, because they know nothing about my custom mode. The ModeChange hook is much more general, though, so I added:

hook global ModeChange .*:spell %{ spell }
hook global ModeChange spell:.* %{ spell-clear }

… and nothing happened. I got curious, so I added a new hook:

hook global ModeChange .* %{ echo -debug ModeChange %val{hook_param} }

… and started playing around. When I hit , to display the list of user modes, my debug log says:

ModeChange normal:next-key

… and when I type a mapping to enter a user-mode, it says:

ModeChange next-key:normal
ModeChange normal:prompt
ModeChange prompt:normal
ModeChange normal:next-key

In a way, that makes sense: the mapping is a normal-mode command, so Kakoune leaves the “next-key” mode and goes back to normal mode. The normal-mode command is : to open the prompt, it types an enter-user-mode command, and then we’re back in next-key mode again. But clearly trying to hook our user-mode by name is not going to work if Kakoune is only ever in next-key mode.

I guess strictly speaking we don’t need a special hook to run commands when entering a mode:

define-command enter-spell-mode %{
    spell
    enter-user-mode -lock spell
}
map global user s ': enter-spell-mode<ret>' -docstring spellcheck

… but the user can leave spell mode at any time with Esc, which I can’t write a mapping for (a good thing).

Alternatively, when entering the mode I could write a one-shot hook for ModeChange next-key:normal, except that Kakoune naturally fires that hook while executing commands within the mode, not just when exiting it.

But of course, that’s what the proposed ModePush and ModePop hooks are for, right? Surely that will let me distinguish “temporarily entering normal mode to run a command” from “permanently returning to normal mode”. As it turns out, no:

ModePush normal:next-key:2  # I hit , to enter the user-mode list
ModePop next-key:normal:2   # I hit s to trigger the "enter spell mode" mapping
ModePush normal:prompt:2    # the mapping runs in normal mode
ModePop prompt:normal:2
ModePush normal:next-key:2  # Kakoune actually enters spell mode
ModePop next-key:normal:2

It seems that “entering” a custom mode is not like entering normal mode from insert mode, it doesn’t add new stack modes, it just shuffles modes around at the same depth.

Is there some clever way I can get the “run commands on entering and exiting a user-mode” that I want, or should I start filing issues?

I don’t think so. What I liked in Vim, is that spell checking works in background (sort of). I think if Kakoune will implement something like

  • User sets some boolean on
  • If it is on, upon switching to normal mode, spell command refreshes current buffer.
  • If user sets this boolean off, spell.kak executes spell-clear

I think, that for writers there’s no point to leave spell checking mode ever.

Also there should be a proper way to add words to dictionary or ignore some words.

Apps like Microsoft Word (or this very forum) do spell-checking all the time because the “incorrect” marker is subtle (a red wavy underline), the user has a rich interface for interacting with it (right-click to open a menu with all kinds of options), and it’s efficient (each word can be checked individually as you type).

Kakoune is in a different category, at least at the moment. The ‘incorrect’ marker is very noisy (bright red background), the user has a very limited interface for interacting with the checker (by default, only :spell-next), and Kakoune has to pipe the entire document into the spell-checker every time.

I very definitely don’t want to have spell-checking enabled all the time while Kakoune has no way to add a word to the dictionary, or mark it as ignored.

It also occurred to me today that a feature like this would also be great for implementing something like git add -p inside Kakoune: enter “stage hunks” mode, automatically run “git show-diff”, have k and j jump between hunks, a to stage the currently-selected hunk, u to unstage it, and when the user hits Esc automatically run “git hide-diff”.

I guess you want. But you also want a better spell checker with more features, commands etc.

A bit off-topic but I just stumbled upon this nice flowchart of Vim modes:
https://rawgit.com/darcyparker/1886716/raw/vimModeStateDiagram.svg

1 Like

It recently came to my attention that Kakoune’s ModeChange hook now does let you distinguish user modes, so it’s possible to automatically execute :spell. I tinkered around a bit and came up with this:

declare-user-mode spell

# This mapping re-checks the document,
# so other instances of the added word won't be marked as errors.
map global spell a ': spell-add; spell<ret>' -docstring 'add to dictionary'
map global spell s ': spell-replace<ret>'    -docstring 'suggest replacements'
map global spell n ': spell-next<ret>'       -docstring 'next misspelling'

# Whenever the user enters the "spell" user mode...
hook global ModeChange push:[^:]*:next-key\[user.spell\] %{
    # ...automatically check the document and highlight errors.
    spell

    # We don't use a ModeChange hook
    # to detect when the user leaves "spell" mode,
    # because Kakoune always leaves "spell" mode when a mapping is executed,
    # even if it's going to re-enter the mode immediately afterward.
    # Instead, we clear spelling errors only once
    # the user returns to Normal mode permanently.
    hook -once -always window NormalIdle .* spell-clear
}

The biggest problem with this solution is the “replace” mapping. spell-replace uses the :menu command to provide a list of suggestions, but because of the way user-mode locking works in Kakoune, the user mode is re-displayed first. As a result, you have to hit <esc> to get out of the user mode before you can navigate the suggestion list.

3 Likes

Hi,

Since this post @Screwtapello made spell.kak use prompt for spell-replace which is quite neat, but the spell-replace overlapping with spell user mode issue remained.

I have tried to solve that, here is my final version, which is not perfect, but fits to my workflow.

declare-user-mode spell
define-command -hidden -params 0 _spell-replace %{
    hook -always -once window ModeChange push:prompt:next-key\[user.spell\] %{
        execute-keys <esc>
    }
    # hook -once -always window ModeChange pop:prompt:normal %{
    #     echo -debug 'DEBUG: user-mode -lock spell hook called.'
    #     enter-user-mode -lock spell
    #     spell
    # }
    hook -once -always window NormalIdle .* %{
        enter-user-mode -lock spell
        spell
    }
    spell-replace
}
map global spell a ': spell-add; spell<ret>' -docstring 'add to dictionary'
map global spell r ': _spell-replace<ret>' -docstring 'suggest replacements'
map global spell n ': spell-next<ret>' -docstring 'next misspelling'
map global spell h ': set current spell_lang hu_HU; spell<ret>' -docstring 'Hungarian check'
map global spell e ': set current spell_lang en_US; spell<ret>' -docstring 'English check'
map global normal <Ă­> ': enter-user-mode -lock spell<ret>'

hook global ModeChange push:[^:]*:next-key\[user.spell\] %{
    hook -once -always window NormalIdle .* spell-clear
}

Explanation:

  • This adds two hooks to spell-replace: the first one leaves the spell user-mode when we call spell-replace (by sending an <esc> key and the second one restarts the spell user mode and calls spell to refresh the spell errors.
  • You can not replace the e and h mappings with :spell hu_HU<ret> because of the spell call in _spell_replace, which uses default locale unless spell_lang is set.
  • The commented part would make it possible to not call spell after every spell-replace, but I am not sure that would be really useful as changing a word could mess up the highlights. (Which probably fixable with update-option, but I am not sure.)
1 Like

Woah…I just started to use spell and began implementing my own user-mode and ran smack into this exact same “prompt” problem. Awesome solution @voroskoi , thanks for sharing!