feat: add Lua API of IME event handling (#3110)

* feat: add preedit support when enabled IME mode

related to neovide#1931

* feat: support multi-line editing when IME is enabled

* feat: add an API for handling IME events

Default action is to insert commited text after the cursor. And, you can
customize the behavior when preeditted or committed.

* Revert to previous behavior

If the text which you input is committed, process as key input
regardless of mode of neovim

* doc: update related contents of IME handling API

* feat: support sending raw text and cursor offset

* doc: add the content to enable preedit of IME

* fix: broken link in faq

* style: format ui_commands.rs

* style: format api.md and faq.md

* style: format api.md again

100 chars line len
This commit is contained in:
kanium 2025-09-10 16:53:45 +09:00 committed by GitHub
parent 24bae1bcce
commit 75ae946835
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 205 additions and 65 deletions

View file

@ -147,4 +147,14 @@ M.enable_redraw = function()
pcall(rpcnotify, "neovide.set_redraw", true)
end
---@param preedit_raw_text string
---@param cursor_offset? [integer, integer] (start_col, end_col) This values show the cursor begin position and end position. The position is byte-wise indexed.
M.preedit_handler = function(preedit_raw_text, cursor_offset) end
---@param commit_raw_text string
---@param commit_formatted_text string It's escaped.
M.commit_handler = function(commit_raw_text, commit_formatted_text)
vim.api.nvim_input(commit_formatted_text)
end
_G["neovide"] = M

View file

@ -23,6 +23,12 @@ use crate::{
#[derive(Clone, Debug, AsRefStr)]
pub enum SerialCommand {
Keyboard(String),
KeyboardAsIme {
formatted: Option<String>,
raw: String,
commit: bool,
cursor_offset: Option<(usize, usize)>,
},
MouseButton {
button: String,
action: String,
@ -59,6 +65,44 @@ impl SerialCommand {
.map(|_| ())
.context("Input failed")
}
SerialCommand::KeyboardAsIme {
formatted,
commit,
raw,
cursor_offset,
} => {
if commit {
// Notified ime commit event, the text is guaranteed not to be None.
let text = formatted.unwrap();
trace!("IME Input Sent: {}", &text);
nvim.exec_lua(
&format!("neovide.commit_handler([[{}]], [[{}]])", raw, text),
vec![],
)
.await
.map(|_| ())
.context("IME Commit failed")
} else {
trace!("IME Input Preedit");
if let Some((start_col, end_col)) = cursor_offset {
nvim.exec_lua(
&format!(
"neovide.preedit_handler([[{}]], {{ {}, {} }})",
raw, start_col, end_col
),
vec![],
)
.await
.map(|_| ())
.context("IME Preedit failed")
} else {
nvim.exec_lua(&format!("neovide.preedit_handler([[{}]])", raw), vec![])
.await
.map(|_| ())
.context("IME Preedit failed")
}
}
}
SerialCommand::MouseButton {
button,
action,

View file

@ -59,10 +59,21 @@ impl KeyboardManager {
}
WindowEvent::Ime(Ime::Commit(text)) => {
log::trace!("Ime commit {text}");
send_ui(SerialCommand::Keyboard(self.format_key_text(text, false)));
send_ui(SerialCommand::KeyboardAsIme {
formatted: Some(self.format_key_text(text, false)),
raw: text.to_owned(),
commit: true,
cursor_offset: None,
});
}
WindowEvent::Ime(Ime::Preedit(text, cursor_offset)) => {
self.ime_preedit = (text.to_string(), *cursor_offset)
self.ime_preedit = (text.to_string(), *cursor_offset);
send_ui(SerialCommand::KeyboardAsIme {
formatted: None,
raw: text.to_owned(),
commit: false,
cursor_offset: *cursor_offset,
});
}
WindowEvent::ModifiersChanged(modifiers) => {
// Record the modifier states so that we can properly add them to the keybinding text

View file

@ -4,8 +4,7 @@ The API fuctions are always available without any imports as long as Neovide is
## Redraw Control
`neovide.disable_redraw()`
`neovide.enable_redraw()`
`neovide.disable_redraw()` `neovide.enable_redraw()`
These can be used to by plugins to temporarily disable redrawing while performing some update. They
can for exapmple, be used to prevent the cursor from temporarily moving to the wrong location, or to
@ -39,3 +38,36 @@ if neovide and neovide.enable_redraw then neovide.enable_redraw() end
**Don't call these functions as a regular user, since you won't see any updates on the screen until
the redrawing is enabled again, so it might be hard to type in the command.**
## IME handling
`neovide.preedit_handler(
preedit_raw_text:string,
cursor_offset:[start_col:integer, end_col:integer]
)`
`neovide.commit_handler(commit_raw_text:string, commit_formatted_text:string)`
These can be used to by your plugin to handle IME events. The pre-edit handler is
called when yourinput method, such as Fcitx, IBus and MS-IME, sends pre-edit event.
So, you have to handle pre-edit texts if you would like to support pre-edit event.
The commit handler is called when your inputmethod sends commit event,
which you decide some text on enabled IME.
In default, `preedit_handler()` is nothing to do and `commit_handler()` uses
[`nvim_input()`](<https://neovim.io/doc/user/api.html#nvim_input()>)
```lua
---@param preedit_raw_text string
---@param cursor_offset? [integer, integer] (start_col, end_col)
--- This values show the cursor begin position and end position.
--- The position is byte-wise indexed.
neovide.preedit_handler = function (preedit_raw_text, cursor_offset)
-- handle pre-edit event...
end
---@param commit_raw_text string
---@param commit_formatted_text string It's escaped.
neovide.commit_handler = function (commit_raw_text, commit_formatted_text)
-- handle commit event...
end
```

View file

@ -4,9 +4,9 @@ Commonly asked questions, or just explanations/elaborations on stuff.
## How can I use cmd-c/cmd-v to copy and paste?
Neovide doesn't add or remove any keybindings to neovim, it only forwards keys. Its likely that
your terminal adds these keybindings, as neovim doesn't have them by default. We can replicate
this behavior by adding keybindings in neovim.
Neovide doesn't add or remove any keybindings to neovim, it only forwards keys. Its likely that your
terminal adds these keybindings, as neovim doesn't have them by default. We can replicate this
behavior by adding keybindings in neovim.
```lua
if vim.g.neovide then
@ -35,12 +35,10 @@ one for the popup menu.
telescope.nvim is different here though. Instead of using the global `winblend` option, it has its
own `telescope.defaults.winblend` configuration option, see [this comment in #1626].
[this comment in #1626]: https://github.com/neovide/neovide/issues/1626#issuecomment-1701080545
## How Can I Dynamically Change The Scale At Runtime?
Neovide offers the setting `g:neovide_scale_factor`, which is multiplied with
the OS scale factor and the font size. So using this could look like
Neovide offers the setting `g:neovide_scale_factor`, which is multiplied with the OS scale factor
and the font size. So using this could look like
VimScript:
@ -113,50 +111,95 @@ vim.keymap.set({ "n", "v", "o" }, "<D-[>", function()
end)
```
## How To Enable Preedit Support Of IME?
By default, the IME preedit event—that is, the preview of the text being composed—is not
implemented. You can implement this preview yourself by using preedit_handler().
Example:
```lua
local ime_context = {
base_col = 0,
base_row = 0,
preedit_col = 0,
preedit_row = 0,
}
---@param preedit_raw_text string
---@param cursor_offset [integer, integer]: [start_col, end_col]
preedit_handler = function(preedit_raw_text, cursor_offset)
vim.api.nvim_buf_set_text(
0,
ime_context.base_row - 1,
ime_context.base_col,
ime_context.preedit_row - 1,
ime_context.preedit_col,
{}
)
ime_context.preedit_col = ime_context.base_col + string.len(preedit_raw_text)
vim.api.nvim_buf_set_text(
0,
ime_context.base_row - 1,
ime_context.base_col,
ime_context.base_row - 1,
ime_context.base_col,
{ preedit_raw_text }
)
vim.api.nvim_win_set_cursor(0, { ime_context.preedit_row, ime_context.preedit_col })
end
```
Neovide also exposes a Lua function called commit_handler() in addition to preedit_handler(). For
details, see [IME handling on the API page](api.html#ime-handling).
If youd prefer not to set this up yourself, you can use
[kanium3/neovide_ime.nvim](https://github.com/kanium3/neovide-ime.nvim). Please refer to that
repository for more information. Example: Installation with Lazy.nvim
```lua
return {
"kanium3/neovide-ime.nvim"
}
```
Related: [PR #3110](https://github.com/neovide/neovide/pull/3110)
## Neovide Is Not Picking Up Some Shell-configured Information
...aka `nvm use` doesn't work, aka anything configured in `~/.bashrc`/`~/.zshrc`
is ignored by Neovide.
...aka `nvm use` doesn't work, aka anything configured in `~/.bashrc`/`~/.zshrc` is ignored by
Neovide.
Neovide doesn't start the embedded neovim instance in an interactive shell, so your
shell doesn't read part of its startup file (`~/.bashrc`/`~/.zshrc`/whatever the
equivalent for your shell is). But depending on your shell there are other
options for doing so, for example for zsh you can just put your relevant content
into `~/.zprofile` or `~/.zlogin`.
Neovide doesn't start the embedded neovim instance in an interactive shell, so your shell doesn't
read part of its startup file (`~/.bashrc`/`~/.zshrc`/whatever the equivalent for your shell is).
But depending on your shell there are other options for doing so, for example for zsh you can just
put your relevant content into `~/.zprofile` or `~/.zlogin`.
## The Terminal Displays Fallback Colors/:terminal Does Not Show My Colors
Your colorscheme has to define `g:terminal_color_0` through
`g:terminal_color_15` in order to have any effect on the terminal. Just setting
any random highlights which have `Term` in name won't help.
Your colorscheme has to define `g:terminal_color_0` through `g:terminal_color_15` in order to have
any effect on the terminal. Just setting any random highlights which have `Term` in name won't help.
Some colorschemes think of this, some don't. Search in the documentation of
yours, if it's your own, add it, and if you can't seem to find anything, open an
issue in the colorscheme's repo.
Some colorschemes think of this, some don't. Search in the documentation of yours, if it's your own,
add it, and if you can't seem to find anything, open an issue in the colorscheme's repo.
## Compose key sequences do not work
One possible cause might be inconsistent capitalization of your locale
settings, see [#1896]. Possibly you're also running an outdated version of
Neovide.
One possible cause might be inconsistent capitalization of your locale settings, see [#1896].
Possibly you're also running an outdated version of Neovide.
[#1896]: https://github.com/neovide/neovide/issues/1896#issuecomment-1616421167.
Another possible cause is that you are using IME on X11. Dead keys with IME is
not yet supported, but you can work around that either by disabling IME or
configuring it to only be enabled in insert mode. See
[Configuration](configuration.md).
Another possible cause is that you are using IME on X11. Dead keys with IME is not yet supported,
but you can work around that either by disabling IME or configuring it to only be enabled in insert
mode. See [Configuration](configuration.md).
## Font size is weird with high dpi display on x11
Winit looks in multiple locations for the configured dpi.
Make sure its set in at least one of them. More details
here: [#2010](https://github.com/neovide/neovide/issues/2010#issuecomment-1704416685).
Winit looks in multiple locations for the configured dpi. Make sure its set in at least one of them.
More details here: [#2010](https://github.com/neovide/neovide/issues/2010#issuecomment-1704416685).
## How to turn off all animations?
Animations can be turned off by setting the following global
variables:
Animations can be turned off by setting the following global variables:
```lua
vim.g.neovide_position_animation_length = 0
@ -170,43 +213,43 @@ vim.g.neovide_scroll_animation_length = 0.00
## macOS Login Shells
Traditionally, Unix shells use two main configuration files that are executed
before a user can interact with the shell: a profile file and an rc file.
Traditionally, Unix shells use two main configuration files that are executed before a user can
interact with the shell: a profile file and an rc file.
- **Profile File:** This file is typically executed once at login to set up
the user's environment.
- **RC File:** This file is executed every time a new shell is created to
configure the shell itself.
- **Profile File:** This file is typically executed once at login to set up the user's environment.
- **RC File:** This file is executed every time a new shell is created to configure the shell
itself.
In the case of Zsh, which has been the default shell on macOS since version
10.15, the configuration files used are `.zprofile` and `.zshrc`.
In the case of Zsh, which has been the default shell on macOS since version 10.15, the configuration
files used are `.zprofile` and `.zshrc`.
### Bash Differences
Unlike Zsh, Bash behaves differently. It only reads `.bashrc` if the shell
session is both interactive and non-login. This distinction might have been
overlooked when macOS transitioned from tcsh to bash in OSX 10.2 Jaguar,
leading developers to place their setup entirely in `.profile` since `.bashrc`
would rarely be executed, especially when starting a new terminal.
Unlike Zsh, Bash behaves differently. It only reads `.bashrc` if the shell session is both
interactive and non-login. This distinction might have been overlooked when macOS transitioned from
tcsh to bash in OSX 10.2 Jaguar, leading developers to place their setup entirely in `.profile`
since `.bashrc` would rarely be executed, especially when starting a new terminal.
With the shift to Zsh as the default shell, both `.zprofile` and `.zshrc` are
executed when starting an interactive non-login shell.
With the shift to Zsh as the default shell, both `.zprofile` and `.zshrc` are executed when starting
an interactive non-login shell.
![pic alt](./assets/login-shell.png)
_Regarding to the moment when Neovide launches, it does not start an
interactive shell session, meaning the .bashrc file is not executed. Instead,
the system reads the .bash_profile file. This behavior stems from the
difference in how interactive and login shells process configuration files._
_Regarding to the moment when Neovide launches, it does not start an interactive shell session,
meaning the .bashrc file is not executed. Instead, the system reads the .bash_profile file. This
behavior stems from the difference in how interactive and login shells process configuration files._
### macOS Specifics
On macOS, the graphical user interface used for system login does not execute
`.zprofile`, as it employs a different method for loading system-level global
settings. This means that terminal emulators must run shells as login shells
to ensure that new shells are properly configured, avoiding potential issues
from missing setup processes in `.zprofile`. This necessity arises because
there is no `.xsession` or equivalent file on macOS to provide initial
settings or global environment variables to terminal sessions[^1].
On macOS, the graphical user interface used for system login does not execute `.zprofile`, as it
employs a different method for loading system-level global settings. This means that terminal
emulators must run shells as login shells to ensure that new shells are properly configured,
avoiding potential issues from missing setup processes in `.zprofile`. This necessity arises because
there is no `.xsession` or equivalent file on macOS to provide initial settings or global
environment variables to terminal sessions[^1].
[^1]: [Why are interactive shells on OSX login shells by default?](https://unix.stackexchange.com/questions/119627/why-are-interactive-shells-on-osx-login-shells-by-default)
\[^1\]:
[Why are interactive shells on OSX login shells by default?](https://unix.stackexchange.com/questions/119627/why-are-interactive-shells-on-osx-login-shells-by-default)
[#1896]: https://github.com/neovide/neovide/issues/1896#issuecomment-1616421167.
[this comment in #1626]: https://github.com/neovide/neovide/issues/1626#issuecomment-1701080545