From 75ae946835b56a31c54d18c0cd228aa9d2a50991 Mon Sep 17 00:00:00 2001 From: kanium <67541916+kanium3@users.noreply.github.com> Date: Wed, 10 Sep 2025 16:53:45 +0900 Subject: [PATCH] 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 --- lua/init.lua | 10 ++ src/bridge/ui_commands.rs | 44 +++++++++ src/window/keyboard_manager.rs | 15 ++- website/docs/api.md | 36 ++++++- website/docs/faq.md | 165 +++++++++++++++++++++------------ 5 files changed, 205 insertions(+), 65 deletions(-) diff --git a/lua/init.lua b/lua/init.lua index cd308b6c..f616d2c2 100644 --- a/lua/init.lua +++ b/lua/init.lua @@ -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 diff --git a/src/bridge/ui_commands.rs b/src/bridge/ui_commands.rs index 67bdcaed..0343a6a2 100644 --- a/src/bridge/ui_commands.rs +++ b/src/bridge/ui_commands.rs @@ -23,6 +23,12 @@ use crate::{ #[derive(Clone, Debug, AsRefStr)] pub enum SerialCommand { Keyboard(String), + KeyboardAsIme { + formatted: Option, + 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, diff --git a/src/window/keyboard_manager.rs b/src/window/keyboard_manager.rs index b9e86df2..27dd1a8b 100644 --- a/src/window/keyboard_manager.rs +++ b/src/window/keyboard_manager.rs @@ -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 diff --git a/website/docs/api.md b/website/docs/api.md index 8b20d5b3..56795a2e 100644 --- a/website/docs/api.md +++ b/website/docs/api.md @@ -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()`]() + +```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 +``` diff --git a/website/docs/faq.md b/website/docs/faq.md index 929b7e78..a6d70ff5 100644 --- a/website/docs/faq.md +++ b/website/docs/faq.md @@ -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" }, "", 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 you’d 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