├── LICENSE ├── README.md └── lua └── ix ├── action.lua ├── init.lua ├── misc.lua └── source.lua /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 hrsh7th 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `nvim-ix` 2 | 3 | insert mode enhancement plugin for Neovim 4 | 5 | Software License 6 | DeepWiki 7 | 8 | **API Stability Warning** 9 | 10 | _The API is not yet stable. If you have made any advanced customizations, they 11 | may stop working without notice._ 12 | 13 | ## Overview 14 | 15 | `nvim-ix` is a plugin for Neovim that provides insert-mode enhancement 16 | functionalities. It internally utilizes the core library `nvim-cmp-kit` to offer 17 | a user-friendly API. 18 | 19 | - **`nvim-ix`**: The interface for user configuration and operation. 20 | - **`nvim-cmp-kit`**: The core engine responsible for the actual completion 21 | logic, such as generating completion candidates and interacting with LSP. 22 | 23 | This architecture allows `nvim-ix` to provide Neovim users with an 24 | easy-to-configure and intuitive experience, while `nvim-cmp-kit` handles complex 25 | processing, achieving both stability and advanced features. 26 | 27 | ## Key Features 28 | 29 | - **Completion**: Input completion in insert mode and command-line mode. 30 | - **Signature Help**: Displays function and method signatures (argument 31 | information, etc.). 32 | - **Built-in Common Sources**: 33 | - **Completion** 34 | - `buffer`: Words from the current buffer. 35 | - `path`: File and directory paths. 36 | - `calc`: Evaluation of simple mathematical expressions. 37 | - `cmdline`: Neovim commands. 38 | - `lsp.completion`: Completion candidates from LSP servers. 39 | - **SignatureHelp** 40 | - `lsp.signature_help`: Signature help from LSP servers. 41 | - **Key-mapping**: `ix.charmap` for setting up keybindings with reduced 42 | conflicts. 43 | - **Pretty Markdown Rendering**: Completion documentation / Signature Help 44 | rendering. 45 | 46 | --- 47 | 48 | ## Installation 49 | 50 | **Prerequisites** 51 | 52 | - Neovim 0.11 or later. 53 | - `nvim-ix` uses `vim.on_key` with empty return string. It's introduced in 54 | Neovim 0.11. 55 | - NerdFonts 56 | - `nvim-ix`'s default view uses NerdFonts. 57 | 58 | **Lazy.nvim example** 59 | 60 | ```lua 61 | -- lazy.nvim 62 | { 63 | "hrsh7th/nvim-ix", 64 | dependencies = { 65 | "hrsh7th/nvim-cmp-kit", 66 | }, 67 | } 68 | ``` 69 | 70 | ## Basic Usage 71 | 72 | To use `nvim-ix`, first call the `setup` function for initial configuration. 73 | 74 | ```lua 75 | vim.o.winborder = 'rounded' -- (Optional) nvim-ix follows global `winborder` settings to render windows 76 | 77 | local ix = require('ix') 78 | 79 | -- Update LSP capabilities 80 | vim.lsp.config('*', { 81 | capabilities = ix.get_capabilities() 82 | }) 83 | 84 | -- Setup nvim-ix 85 | ix.setup({ 86 | -- Register snippet expand function (optional if not using snippets) 87 | expand_snippet = function(snippet_body) 88 | -- vim.snippet.expand(snippet) -- for `neovim built-in` users 89 | -- require('luasnip').lsp_expand(snippet) -- for `LuaSnip` users 90 | -- require('snippy').expand_snippet(snippet) -- for `nvim-snippy` users 91 | -- vim.fn["vsnip#anonymous"](snippet_body) -- for `vim-vsnip` users 92 | end 93 | }) 94 | 95 | -- Setup keymaps (Using `ix.charmap`; See below). 96 | do 97 | ix.charmap({ 'i', 'c' }, '', ix.action.scroll(0 + 3)) 98 | ix.charmap({ 'i', 'c' }, '', ix.action.scroll(0 - 3)) 99 | 100 | ix.charmap({ 'i', 'c' }, '', ix.action.completion.complete()) 101 | ix.charmap({ 'i', 'c' }, '', ix.action.completion.select_next()) 102 | ix.charmap({ 'i', 'c' }, '', ix.action.completion.select_prev()) 103 | ix.charmap({ 'i', 'c' }, '', ix.action.completion.close()) 104 | ix.charmap('c', '', ix.action.completion.commit_cmdline()) 105 | ix.charmap('i', '', ix.action.completion.commit({ select_first = true })) 106 | ix.charmap('i', '', ix.action.completion.select_next()) 107 | ix.charmap('i', '', ix.action.completion.select_prev()) 108 | ix.charmap('i', '', ix.action.completion.commit({ select_first = true, replace = true, no_snippet = true })) 109 | 110 | ix.charmap({ 'i', 's' }, '', ix.action.signature_help.trigger_or_close()) 111 | ix.charmap({ 'i', 's' }, '', ix.action.signature_help.select_next()) 112 | end 113 | ``` 114 | 115 | **Regarding LSP Capabilities Update**: 116 | 117 | `ix.get_capabilities()` is returning LSP Capabilities that `nvim-ix` supports. 118 | 119 | The LSP specification defines the concept of `capabilities`, which an `editor` can use to inform the server that it supports the features defined in the LSP. 120 | 121 | `nvim-ix` supports a variety of features related to completion and signature help, so please inform the LSP server. 122 | 123 | **Snippet Engine Integration**: 124 | 125 | Specify your snippet engine's expansion function with the `expand_snippet` 126 | option. If not provided, snippet-related functionalities will be disabled. 127 | 128 | **Key-mapping with `ix.charmap`** 129 | 130 | `ix.charmap` is a utility for easily setting up key-mappings for the plugin's 131 | main operations. It helps avoid key conflicts with other plugins. 132 | 133 | **`ix.setup({ ... })` reference** 134 | 135 | The following setup call indicates all default settings. 136 | 137 |
138 | 139 | default configuration 140 | 141 | ```lua 142 | local ix = require('nvim-ix') 143 | ix.setup({ 144 | ---Expand snippet function. 145 | ---@type nil|cmp-kit.completion.ExpandSnippet 146 | expand_snippet = nil, 147 | 148 | ---Completion configuration. 149 | completion = { 150 | 151 | ---Enable/disable auto completion. 152 | ---@type boolean 153 | auto = true, 154 | 155 | ---Enable/disable LSP's preselect feature. 156 | ---@type boolean 157 | preselect = false, 158 | 159 | ---Default keyword pattern for completion. 160 | ---@type string 161 | default_keyword_pattern = require('cmp-kit.completion.ext.DefaultConfig').default_keyword_pattern, 162 | 163 | ---Resolve LSP's CompletionItemKind to icons. 164 | ---@type nil|fun(kind: cmp-kit.kit.LSP.CompletionItemKind): { [1]: string, [2]?: string }? 165 | icon_resolver = (function() 166 | local cache = {} 167 | 168 | local CompletionItemKindLookup = {} 169 | for k, v in pairs(LSP.CompletionItemKind) do 170 | CompletionItemKindLookup[v] = k 171 | end 172 | 173 | -- For mini.icons 174 | local ok, MiniIcons = pcall(require, 'mini.icons') 175 | if ok and MiniIcons then 176 | ---@param completion_item_kind cmp-kit.kit.LSP.CompletionItemKind 177 | ---@return { [1]: string, [2]?: string }? 178 | return function(completion_item_kind) 179 | if not cache[completion_item_kind] then 180 | local kind = CompletionItemKindLookup[completion_item_kind] or 'text' 181 | cache[completion_item_kind] = { MiniIcons.get('lsp', kind:lower()) } 182 | end 183 | return cache[completion_item_kind] 184 | end 185 | end 186 | return nil 187 | end)(), 188 | }, 189 | 190 | ---Signature help configuration. 191 | signature_help = { 192 | 193 | ---Auto trigger signature help. 194 | ---@type boolean 195 | auto = true, 196 | 197 | }, 198 | 199 | ---Attach services for each per modes. 200 | attach = { 201 | 202 | ---Insert mode service initialization. 203 | ---NOTE: This is an advanced feature and is subject to breaking changes as the API is not yet stable. 204 | ---@type fun(): nil 205 | insert_mode = function() 206 | do 207 | local service = ix.get_completion_service({ recreate = true }) 208 | service:register_source(ix.source.completion.calc(), { group = 1 }) 209 | service:register_source(ix.source.completion.path(), { group = 10 }) 210 | ix.source.completion.attach_lsp(service, { group = 20 }) 211 | service:register_source(ix.source.completion.buffer(), { group = 100 }) 212 | end 213 | do 214 | local service = ix.get_signature_help_service({ recreate = true }) 215 | ix.source.signature_help.attach_lsp(service) 216 | end 217 | end, 218 | 219 | ---Cmdline mode service initialization. 220 | ---NOTE: This is an advanced feature and is subject to breaking changes as the API is not yet stable. 221 | ---@type fun(): nil 222 | cmdline_mode = function() 223 | local service = ix.get_completion_service({ recreate = true }) 224 | if vim.tbl_contains({ '/', '?' }, vim.fn.getcmdtype()) then 225 | service:register_source(ix.source.completion.buffer(), { group = 1 }) 226 | elseif vim.fn.getcmdtype() == ':' then 227 | service:register_source(ix.source.completion.path(), { group = 1 }) 228 | service:register_source(ix.source.completion.cmdline(), { group = 10 }) 229 | end 230 | end, 231 | 232 | } 233 | }) 234 | ``` 235 | 236 |
237 | 238 | --- 239 | 240 | ## Advanced Usage 241 | 242 | **Call `nvim-ix` actions anywhere** 243 | 244 | It is possible to call `nvim-ix` actions without `ix.charmap` for integrating 245 | some of the your specific workflows. 246 | 247 | ```lua 248 | vim.keymap.set('i', '', function() 249 | ix.do_action(function(ctx) 250 | ctx.completion.complete() 251 | end) 252 | end) 253 | ``` 254 | 255 | --- 256 | 257 | ## FAQ 258 | 259 | **Why is `ix.charmap` needed?** 260 | 261 | Keys like `` and `` are prone to conflicts as they are used for 262 | multiple functions (e.g., confirming completion, inserting a newline, expanding 263 | a snippet). `ix.charmap` aims to handle `nvim-ix` actions for these keys with 264 | higher precedence than other mappings, reducing conflicts and ensuring reliable 265 | behavior. 266 | 267 | **How can I set up key-mappings without using `ix.charmap`?** 268 | 269 | You can use `ix.do_action` for this. 270 | 271 | ```lua 272 | vim.keymap.set('i', '', function() 273 | ix.do_action(function(ctx) 274 | ctx.completion.complete() 275 | end) 276 | end) 277 | ``` 278 | 279 | **Why create a new completion plugin?** 280 | 281 | `nvim-ix` was developed based on the experience from existing completion plugins 282 | (like `nvim-cmp`), aiming for a different architectural approach (adoption of 283 | the core engine `nvim-cmp-kit`) and an API design more compliant with LSP 284 | specifications. 285 | -------------------------------------------------------------------------------- /lua/ix/action.lua: -------------------------------------------------------------------------------- 1 | local action = {} 2 | 3 | --- common. 4 | do 5 | ---Scroll completion docs or signature help. 6 | function action.scroll(delta) 7 | ---@type ix.Charmap.Callback 8 | return function(ctx, fallback) 9 | local exec = false 10 | if ctx.completion.is_docs_visible() then 11 | ctx.completion.scroll_docs(delta) 12 | exec = true 13 | end 14 | if ctx.signature_help.is_visible() then 15 | ctx.signature_help.scroll(delta) 16 | exec = true 17 | end 18 | if not exec then 19 | fallback() 20 | end 21 | end 22 | end 23 | end 24 | 25 | --- completion. 26 | do 27 | action.completion = {} 28 | 29 | ---Invoke completion. 30 | function action.completion.complete() 31 | ---@type ix.Charmap.Callback 32 | return function(ctx) 33 | ctx.completion.complete({ force = true }) 34 | end 35 | end 36 | 37 | ---Select next completion item. 38 | ---@param option? { no_insert?: boolean } 39 | function action.completion.select_next(option) 40 | option = option or {} 41 | option.no_insert = option.no_insert or false 42 | 43 | ---@type ix.Charmap.Callback 44 | return function(ctx, fallback) 45 | local selection = ctx.completion.get_selection() 46 | if selection then 47 | ctx.completion.select(selection.index + 1, option.no_insert) 48 | else 49 | fallback() 50 | end 51 | end 52 | end 53 | 54 | ---Select prev completion item. 55 | ---@param option? { no_insert?: boolean } 56 | function action.completion.select_prev(option) 57 | option = option or {} 58 | option.no_insert = option.no_insert or false 59 | 60 | ---@type ix.Charmap.Callback 61 | return function(ctx, fallback) 62 | local selection = ctx.completion.get_selection() 63 | if selection then 64 | ctx.completion.select(selection.index - 1, option.no_insert) 65 | else 66 | fallback() 67 | end 68 | end 69 | end 70 | 71 | ---Commit completion item. 72 | ---@param option? { select_first?: boolean, replace?: boolean, no_snippet?: boolean } 73 | function action.completion.commit(option) 74 | option = option or {} 75 | option.select_first = option.select_first or false 76 | option.replace = option.replace or false 77 | option.no_snippet = option.no_snippet or false 78 | 79 | ---@type ix.Charmap.Callback 80 | return function(ctx, fallback) 81 | local selection = ctx.completion.get_selection() 82 | if selection then 83 | local index = selection.index 84 | if option.select_first and index == 0 then 85 | index = 1 86 | end 87 | 88 | if index > 0 then 89 | if ctx.completion.commit(index, { replace = option.replace, no_snippet = option.no_snippet }) then 90 | return 91 | end 92 | end 93 | fallback() 94 | end 95 | end 96 | end 97 | 98 | ---Commit completion for cmdline. 99 | function action.completion.commit_cmdline() 100 | ---@type ix.Charmap.Callback 101 | return function(ctx) 102 | ctx.completion.close() 103 | vim.api.nvim_feedkeys(vim.keycode(''), 'n', true) -- don't use `ctx.fallback` here it sends extra `...` keys, that prevent Hit-Enter prompt unexpectedly. 104 | end 105 | end 106 | 107 | ---Close completion menu. 108 | function action.completion.close() 109 | ---@type ix.Charmap.Callback 110 | return function(ctx, fallback) 111 | if ctx.completion.is_menu_visible() then 112 | ctx.completion.close() 113 | else 114 | fallback() 115 | end 116 | end 117 | end 118 | 119 | ---Scroll completion docs. 120 | ---@param delta integer 121 | function action.completion.scroll_docs(delta) 122 | ---@type ix.Charmap.Callback 123 | return function(ctx, fallback) 124 | if ctx.completion.is_docs_visible() then 125 | ctx.completion.scroll_docs(delta) 126 | else 127 | fallback() 128 | end 129 | end 130 | end 131 | end 132 | 133 | --- signature_help. 134 | do 135 | action.signature_help = {} 136 | 137 | ---Trigger signature help. 138 | function action.signature_help.trigger() 139 | ---@type ix.Charmap.Callback 140 | return function(ctx) 141 | ctx.signature_help.trigger({ force = true }) 142 | end 143 | end 144 | 145 | ---Close signature help. 146 | function action.signature_help.close() 147 | ---@type ix.Charmap.Callback 148 | return function(ctx) 149 | ctx.signature_help.close() 150 | end 151 | end 152 | 153 | ---Trigger or close signature help. 154 | function action.signature_help.trigger_or_close() 155 | ---@type ix.Charmap.Callback 156 | return function(ctx) 157 | if ctx.signature_help.is_visible() then 158 | ctx.signature_help.close() 159 | else 160 | ctx.signature_help.trigger({ force = true }) 161 | end 162 | end 163 | end 164 | 165 | ---Select next signature help item. 166 | function action.signature_help.select_next() 167 | ---@type ix.Charmap.Callback 168 | return function(ctx, fallback) 169 | if ctx.signature_help.is_visible() then 170 | local data = ctx.signature_help.get_active_signature_data() 171 | if data then 172 | local index = data.signature_index + 1 173 | if index > data.signature_count then 174 | index = 1 175 | end 176 | ctx.signature_help.select(index) 177 | end 178 | else 179 | fallback() 180 | end 181 | end 182 | end 183 | 184 | ---Select prev signature help item. 185 | function action.signature_help.select_prev() 186 | ---@type ix.Charmap.Callback 187 | return function(ctx, fallback) 188 | if ctx.signature_help.is_visible() then 189 | local data = ctx.signature_help.get_active_signature_data() 190 | if data then 191 | local index = data.signature_index - 1 192 | if index < 1 then 193 | index = data.signature_count 194 | end 195 | ctx.signature_help.select(index) 196 | end 197 | else 198 | fallback() 199 | end 200 | end 201 | end 202 | 203 | ---Scroll signature help view. 204 | function action.signature_help.scroll(delta) 205 | ---@type ix.Charmap.Callback 206 | return function(ctx, fallback) 207 | if ctx.signature_help.is_visible() then 208 | ctx.signature_help.scroll(delta) 209 | else 210 | fallback() 211 | end 212 | end 213 | end 214 | end 215 | 216 | return action 217 | -------------------------------------------------------------------------------- /lua/ix/init.lua: -------------------------------------------------------------------------------- 1 | local misc = require('ix.misc') 2 | local kit = require('cmp-kit.kit') 3 | local LSP = require('cmp-kit.kit.LSP') 4 | local Async = require('cmp-kit.kit.Async') 5 | local Keymap = require('cmp-kit.kit.Vim.Keymap') 6 | local CompletionService = require('cmp-kit.completion.CompletionService') 7 | local SignatureHelpService = require('cmp-kit.signature_help.SignatureHelpService') 8 | 9 | ---@alias ix.Charmap.Callback fun(api: ix.API, fallback: fun()) 10 | 11 | ---@class ix.Charmap 12 | ---@field mode string[] 13 | ---@field char string 14 | ---@field callback ix.Charmap.Callback 15 | 16 | local ix = { 17 | source = require('ix.source'), 18 | action = require('ix.action'), 19 | } 20 | 21 | local private = { 22 | ---completion service registry. 23 | completion = { 24 | i = {} --[[@type table]], 25 | c = {} --[[@type table]], 26 | }, 27 | 28 | ---signature help service registry. 29 | signature_help = { 30 | i = {} --[[@type table]], 31 | c = {} --[[@type table]], 32 | }, 33 | 34 | ---charmaps registry. 35 | charmaps = {} --[=[@as ix.Charmap[]]=], 36 | 37 | ---setup registry. 38 | setup = { 39 | config = {}, 40 | dispose = {}, 41 | } 42 | } 43 | 44 | local default_config = { 45 | ---Expand snippet function. 46 | ---@type nil|cmp-kit.completion.ExpandSnippet 47 | expand_snippet = nil, 48 | 49 | ---Completion configuration. 50 | completion = { 51 | 52 | ---Enable/disable auto completion. 53 | ---@type boolean 54 | auto = true, 55 | 56 | ---Enable/disable LSP's preselect feature. 57 | ---@type boolean 58 | preselect = false, 59 | 60 | ---Default keyword pattern for completion. 61 | ---@type string 62 | default_keyword_pattern = require('cmp-kit.completion.ext.DefaultConfig').default_keyword_pattern, 63 | 64 | ---Resolve LSP's CompletionItemKind to icons. 65 | ---@type nil|fun(kind: cmp-kit.kit.LSP.CompletionItemKind): { [1]: string, [2]?: string }? 66 | icon_resolver = (function() 67 | local cache = {} 68 | 69 | local CompletionItemKindLookup = {} 70 | for k, v in pairs(LSP.CompletionItemKind) do 71 | CompletionItemKindLookup[v] = k 72 | end 73 | 74 | local mini_icons = { pcall(require, 'mini.icons') } 75 | local function update() 76 | if mini_icons[1] then 77 | return 78 | end 79 | mini_icons = { pcall(require, 'mini.icons') } 80 | end 81 | vim.api.nvim_create_autocmd({ 'BufEnter', 'CmdlineEnter' }, { 82 | callback = update, 83 | }) 84 | 85 | -- mini.icons 86 | ---@param completion_item_kind cmp-kit.kit.LSP.CompletionItemKind 87 | ---@return { [1]: string, [2]?: string }? 88 | return function(completion_item_kind) 89 | if mini_icons[1] then 90 | if not cache[completion_item_kind] then 91 | local kind = CompletionItemKindLookup[completion_item_kind] or 'text' 92 | cache[completion_item_kind] = { mini_icons[2].get('lsp', kind:lower()) } 93 | end 94 | return cache[completion_item_kind] 95 | end 96 | return { '', '' } 97 | end 98 | end)(), 99 | }, 100 | 101 | ---Signature help configuration. 102 | signature_help = { 103 | 104 | ---Auto trigger signature help. 105 | ---@type boolean 106 | auto = true, 107 | 108 | }, 109 | 110 | ---Attach services for each per modes. 111 | attach = { 112 | ---Insert mode service initialization. 113 | ---NOTE: This is an advanced feature and is subject to breaking changes as the API is not yet stable. 114 | ---@type fun(): nil 115 | insert_mode = function() 116 | do 117 | local service = ix.get_completion_service({ recreate = true }) 118 | service:register_source(ix.source.completion.calc(), { group = 1 }) 119 | service:register_source(ix.source.completion.path(), { group = 10 }) 120 | ix.source.completion.attach_lsp(service, { group = 20 }) 121 | service:register_source(ix.source.completion.buffer(), { group = 100 }) 122 | end 123 | do 124 | local service = ix.get_signature_help_service({ recreate = true }) 125 | ix.source.signature_help.attach_lsp(service) 126 | end 127 | end, 128 | ---Cmdline mode service initialization. 129 | ---NOTE: This is an advanced feature and is subject to breaking changes as the API is not yet stable. 130 | ---@type fun(): nil 131 | cmdline_mode = function() 132 | local service = ix.get_completion_service({ recreate = true }) 133 | if vim.tbl_contains({ '/', '?' }, vim.fn.getcmdtype()) then 134 | service:register_source(ix.source.completion.buffer(), { group = 1 }) 135 | elseif vim.fn.getcmdtype() == ':' then 136 | service:register_source(ix.source.completion.path(), { group = 1 }) 137 | service:register_source(ix.source.completion.cmdline(), { group = 10 }) 138 | end 139 | end, 140 | } 141 | } --[[@as ix.SetupOption]] 142 | 143 | ---@class ix.SetupOption.Completion 144 | ---@field public auto? boolean 145 | ---@field public default_keyword_pattern? string 146 | ---@field public preselect? boolean 147 | ---@field public icon_resolver? fun(kind: cmp-kit.kit.LSP.CompletionItemKind): { [1]: string, [2]?: string }? 148 | 149 | ---@class ix.SetupOption.SignatureHelp 150 | ---@field public auto? boolean 151 | --- 152 | ---@class ix.SetupOption.Attach 153 | ---@field public insert_mode? fun() 154 | ---@field public cmdline_mode? fun() 155 | --- 156 | ---@class ix.SetupOption 157 | ---@field public expand_snippet? cmp-kit.completion.ExpandSnippet 158 | ---@field public completion? ix.SetupOption.Completion 159 | ---@field public signature_help? ix.SetupOption.SignatureHelp 160 | ---@field public attach? ix.SetupOption.Attach 161 | 162 | ---Setup ix module. 163 | ---@param config? ix.SetupOption 164 | function ix.setup(config) 165 | private.config = kit.merge(config or {}, default_config) 166 | 167 | -- Dispose existing services. 168 | for k, service in pairs(private.completion.i) do 169 | service:dispose() 170 | private.completion.i[k] = nil 171 | end 172 | for k, service in pairs(private.completion.c) do 173 | service:dispose() 174 | private.completion.c[k] = nil 175 | end 176 | for k, service in pairs(private.signature_help.i) do 177 | service:dispose() 178 | private.signature_help.i[k] = nil 179 | end 180 | for k, service in pairs(private.signature_help.c) do 181 | service:dispose() 182 | private.signature_help.c[k] = nil 183 | end 184 | 185 | ---Dispose previous setup. 186 | for _, dispose in ipairs(private.setup.dispose) do 187 | dispose() 188 | end 189 | private.setup.dispose = {} 190 | 191 | ---Setup commands. 192 | ---@diagnostic disable-next-line: duplicate-set-field 193 | vim.lsp.commands['editor.action.triggerParameterHints'] = function() 194 | ix.do_action(function(ctx) 195 | ctx.signature_help.trigger({ force = true }) 196 | end) 197 | end 198 | ---@diagnostic disable-next-line: duplicate-set-field 199 | vim.lsp.commands['editor.action.triggerSuggest'] = function() 200 | ix.do_action(function(ctx) 201 | ctx.completion.complete({ force = true }) 202 | end) 203 | end 204 | 205 | ---Setup char mapping. 206 | do 207 | vim.on_key(function(_, typed) 208 | if not typed or typed == '' then 209 | return 210 | end 211 | local mode = vim.api.nvim_get_mode().mode 212 | 213 | 214 | -- find charmap. 215 | local charmap = vim.iter(private.charmaps):find(function(charmap) 216 | return vim.tbl_contains(charmap.mode, mode) and vim.fn.keytrans(typed) == vim.fn.keytrans(charmap.char) 217 | end) --[[@as ix.Charmap?]] 218 | if not charmap then 219 | return 220 | end 221 | 222 | -- remove typeahead. 223 | while true do 224 | local c = vim.fn.getcharstr(0) 225 | if c == '' then 226 | break 227 | end 228 | end 229 | 230 | ix.do_action(function(ctx) 231 | charmap.callback(ctx, function() 232 | Keymap.send({ keys = charmap.char, remap = true }):await() 233 | end) 234 | end) 235 | 236 | return '' 237 | end, vim.api.nvim_create_namespace('ix'), {}) 238 | end 239 | 240 | ---Setup insert-mode trigger. 241 | do 242 | local queue = misc.autocmd_queue() 243 | table.insert(private.setup.dispose, misc.autocmd({ 'TextChangedI', 'CursorMovedI' }, { 244 | callback = function() 245 | local completion_service = ix.get_completion_service() 246 | local signature_help_service = ix.get_signature_help_service() 247 | queue.add(function() 248 | local mode = vim.api.nvim_get_mode().mode 249 | if vim.tbl_contains({ 'i' }, mode) then 250 | if private.config.completion.auto or completion_service:is_menu_visible() then 251 | completion_service:complete({ force = false }) 252 | end 253 | end 254 | if vim.tbl_contains({ 'i', 's' }, mode) then 255 | if private.config.signature_help.auto or signature_help_service:is_visible() then 256 | signature_help_service:trigger({ force = false }) 257 | end 258 | end 259 | end) 260 | end 261 | })) 262 | table.insert(private.setup.dispose, misc.autocmd('ModeChanged', { 263 | pattern = { 'i:*', 's:*' }, 264 | callback = function() 265 | local completion_service = ix.get_completion_service() 266 | local signature_help_service = ix.get_signature_help_service() 267 | queue.add(function() 268 | local mode = vim.api.nvim_get_mode().mode 269 | if not vim.tbl_contains({ 'i' }, mode) then 270 | completion_service:clear() 271 | end 272 | if not vim.tbl_contains({ 'i', 's' }, mode) then 273 | signature_help_service:clear() 274 | elseif vim.tbl_contains({ 's' }, mode) then 275 | signature_help_service:trigger({ force = true }) 276 | end 277 | end) 278 | end 279 | })) 280 | end 281 | 282 | ---Setup cmdline-mode trigger. 283 | do 284 | local queue = misc.autocmd_queue() 285 | table.insert(private.setup.dispose, misc.autocmd('CmdlineChanged', { 286 | callback = function() 287 | local completion_service = ix.get_completion_service() 288 | local signature_help_service = ix.get_signature_help_service() 289 | queue.add(function() 290 | local mode = vim.api.nvim_get_mode().mode 291 | if mode == 'c' then 292 | if private.config.completion.auto or completion_service:is_menu_visible() then 293 | completion_service:complete({ force = false }) 294 | end 295 | if private.config.signature_help.auto or signature_help_service:is_visible() then 296 | signature_help_service:trigger({ force = false }) 297 | end 298 | end 299 | end) 300 | end 301 | })) 302 | table.insert(private.setup.dispose, misc.autocmd('CmdlineLeave', { 303 | callback = function() 304 | local completion_service = ix.get_completion_service() 305 | local signature_help_service = ix.get_signature_help_service() 306 | queue.add(function() 307 | local mode = vim.api.nvim_get_mode().mode 308 | if mode ~= 'c' then 309 | completion_service:clear() 310 | signature_help_service:clear() 311 | end 312 | end) 313 | end 314 | })) 315 | end 316 | 317 | ---Setup inesrt-mode service initialization. 318 | do 319 | local queue = misc.autocmd_queue() 320 | table.insert(private.setup.dispose, misc.autocmd('BufEnter', { 321 | callback = function() 322 | queue.add(function() 323 | if private.config.attach.insert_mode then 324 | private.config.attach.insert_mode() 325 | end 326 | end) 327 | end 328 | })) 329 | if private.config.attach.insert_mode then 330 | private.config.attach.insert_mode() 331 | end 332 | end 333 | 334 | ---Setup cmdline-mode service initialization. 335 | do 336 | local queue = misc.autocmd_queue() 337 | table.insert(private.setup.dispose, misc.autocmd('CmdlineEnter', { 338 | callback = function() 339 | queue.add(function() 340 | local mode = vim.api.nvim_get_mode().mode 341 | if mode == 'c' then 342 | if private.config.attach.cmdline_mode then 343 | private.config.attach.cmdline_mode() 344 | end 345 | end 346 | end) 347 | end 348 | })) 349 | if vim.api.nvim_get_mode().mode == 'c' then 350 | if private.config.attach.cmdline_mode then 351 | private.config.attach.cmdline_mode() 352 | end 353 | end 354 | end 355 | end 356 | 357 | ---Get current completion service. 358 | ---@param option? { recreate?: boolean } 359 | ---@return cmp-kit.completion.CompletionService 360 | function ix.get_completion_service(option) 361 | option = option or {} 362 | option.recreate = option.recreate or false 363 | 364 | -- cmdline mode. 365 | if vim.api.nvim_get_mode().mode == 'c' then 366 | local key = vim.fn.getcmdtype() 367 | if not private.completion.c[key] or option.recreate then 368 | if private.completion.c[key] then 369 | private.completion.c[key]:dispose() 370 | end 371 | private.completion.c[key] = CompletionService.new({ 372 | default_keyword_pattern = private.config.completion.default_keyword_pattern, 373 | preselect = private.config.completion.preselect, 374 | view = require('cmp-kit.completion.ext.DefaultView').new({ 375 | icon_resolver = private.config.completion.icon_resolver, 376 | }) 377 | }) 378 | end 379 | return private.completion.c[key] 380 | end 381 | 382 | -- insert mode. 383 | local key = vim.api.nvim_get_current_buf() 384 | if not private.completion.i[key] or option.recreate then 385 | if private.completion.i[key] then 386 | private.completion.i[key]:dispose() 387 | end 388 | private.completion.i[key] = CompletionService.new({ 389 | default_keyword_pattern = private.config.completion.default_keyword_pattern, 390 | preselect = private.config.completion.preselect, 391 | expand_snippet = private.config.expand_snippet, 392 | view = require('cmp-kit.completion.ext.DefaultView').new({ 393 | icon_resolver = private.config.completion.icon_resolver, 394 | }) 395 | }) 396 | end 397 | return private.completion.i[key] 398 | end 399 | 400 | ---Get current signature_help service. 401 | ---@param option? { recreate?: boolean } 402 | ---@return cmp-kit.signature_help.SignatureHelpService 403 | function ix.get_signature_help_service(option) 404 | option = option or {} 405 | option.recreate = option.recreate or false 406 | 407 | -- cmdline mode. 408 | if vim.api.nvim_get_mode().mode == 'c' then 409 | local key = vim.fn.getcmdtype() 410 | if not private.signature_help.c[key] or option.recreate then 411 | if private.signature_help.c[key] then 412 | private.signature_help.c[key]:dispose() 413 | end 414 | private.signature_help.c[key] = SignatureHelpService.new({ 415 | view = require('cmp-kit.signature_help.ext.DefaultView').new(), 416 | }) 417 | end 418 | return private.signature_help.c[key] 419 | end 420 | 421 | -- insert mode. 422 | local key = vim.api.nvim_get_current_buf() 423 | if not private.signature_help.i[key] or option.recreate then 424 | if private.signature_help.i[key] then 425 | private.signature_help.i[key]:dispose() 426 | end 427 | private.signature_help.i[key] = SignatureHelpService.new({ 428 | view = require('cmp-kit.signature_help.ext.DefaultView').new(), 429 | }) 430 | end 431 | return private.signature_help.i[key] 432 | end 433 | 434 | ---Setup character mapping. 435 | ---@param mode 'i' | 'c' | 's' | ('i' | 'c' | 's')[] 436 | ---@param char string 437 | ---@param callback fun(api: ix.API, fallback: fun()) 438 | function ix.charmap(mode, char, callback) 439 | local l = 0 440 | local i = 1 441 | local n = false 442 | while i <= #char do 443 | local c = char:sub(i, i) 444 | if c == '<' then 445 | n = true 446 | elseif c == '\\' then 447 | i = i + 1 448 | else 449 | if n then 450 | if c == '>' then 451 | n = false 452 | l = l + 1 453 | end 454 | else 455 | l = l + 1 456 | end 457 | end 458 | i = i + 1 459 | end 460 | 461 | if l > 1 then 462 | error('`ix.charmap` does not support multiple key sequence') 463 | end 464 | 465 | table.insert(private.charmaps, { 466 | mode = kit.to_array(mode), 467 | char = vim.keycode(char), 468 | callback = callback, 469 | }) 470 | end 471 | 472 | ---Run ix action in async-context. 473 | ---@class ix.API.Completion 474 | ---@field prevent fun(callback: fun()) 475 | ---@field close fun() 476 | ---@field is_menu_visible fun(): boolean 477 | ---@field is_docs_visible fun(): boolean 478 | ---@field get_selection fun(): cmp-kit.completion.Selection|nil 479 | ---@field complete fun(option?: { force?: boolean }) 480 | ---@field select fun(index: integer, preselect?: boolean) 481 | ---@field scroll_docs fun(delta: integer) 482 | ---@field commit fun(index: integer, option?: { replace: boolean, no_snippet: boolean }): boolean 483 | ---@class ix.API.SignatureHelp 484 | ---@field prevent fun(callback: fun()) 485 | ---@field trigger fun(option?: { force?: boolean }) 486 | ---@field close fun() 487 | ---@field is_visible fun(): boolean 488 | ---@field get_active_signature_data fun(): cmp-kit.signature_help.ActiveSignatureData|nil 489 | ---@field select fun(index: integer) 490 | ---@field scroll fun(delta: integer) 491 | ---@class ix.API 492 | ---@field completion ix.API.Completion 493 | ---@field signature_help ix.API.SignatureHelp 494 | ---@field schedule fun() 495 | ---@field feedkeys fun(keys: string, remap?: boolean) 496 | 497 | 498 | ---Run ix action with given runner. 499 | ---@param runner fun(ctx: ix.API) 500 | function ix.do_action(runner) 501 | local ctx 502 | ctx = { 503 | completion = { 504 | prevent = function(callback) 505 | local resume = ix.get_completion_service():prevent() 506 | callback() 507 | resume() 508 | end, 509 | close = function() 510 | ix.get_completion_service():clear() 511 | end, 512 | is_menu_visible = function() 513 | return ix.get_completion_service():is_menu_visible() 514 | end, 515 | is_docs_visible = function() 516 | return ix.get_completion_service():is_docs_visible() 517 | end, 518 | get_selection = function() 519 | return ix.get_completion_service():get_selection() 520 | end, 521 | complete = function(option) 522 | ix.get_completion_service():complete(option):await() 523 | end, 524 | select = function(index, preselect) 525 | ix.get_completion_service():select(index, preselect):await() 526 | end, 527 | scroll_docs = function(delta) 528 | ix.get_completion_service():scroll_docs(delta) 529 | end, 530 | commit = function(index, option) 531 | local match = ix.get_completion_service():get_match_at(index) 532 | if match then 533 | ix.get_completion_service():commit(match.item, option):await() 534 | return true 535 | end 536 | return false 537 | end, 538 | }, 539 | signature_help = { 540 | prevent = function(callback) 541 | local resume = ix.get_signature_help_service():prevent() 542 | callback() 543 | resume() 544 | end, 545 | trigger = function(option) 546 | ix.get_signature_help_service():trigger(option):await() 547 | end, 548 | close = function() 549 | ix.get_signature_help_service():clear() 550 | end, 551 | is_visible = function() 552 | return ix.get_signature_help_service():is_visible() 553 | end, 554 | get_active_signature_data = function() 555 | return ix.get_signature_help_service():get_active_signature_data() 556 | end, 557 | select = function(index) 558 | ix.get_signature_help_service():select(index) 559 | end, 560 | scroll = function(delta) 561 | ix.get_signature_help_service():scroll(delta) 562 | end, 563 | }, 564 | schedule = function() 565 | Async.schedule():await() 566 | end, 567 | feedkeys = function(keys, remap) 568 | Keymap.send({ { keys = keys, remap = not not remap } }):await() 569 | end, 570 | } --[[@as ix.API]] 571 | Async.run(function() 572 | runner(ctx) 573 | end) 574 | end 575 | 576 | ---Get ix supported capabilities. 577 | function ix.get_capabilities() 578 | return { 579 | textDocument = { 580 | completion = { 581 | dynamicRegistration = true, 582 | completionItem = { 583 | snippetSupport = true, 584 | commitCharactersSupport = true, 585 | deprecatedSupport = true, 586 | preselectSupport = true, 587 | tagSupport = { 588 | valueSet = { 589 | 1, -- Deprecated 590 | } 591 | }, 592 | insertReplaceSupport = true, 593 | resolveSupport = { 594 | properties = { 595 | "documentation", 596 | "additionalTextEdits", 597 | "insertTextFormat", 598 | "insertTextMode", 599 | "command", 600 | }, 601 | }, 602 | insertTextModeSupport = { 603 | valueSet = { 604 | 1, -- asIs 605 | 2, -- adjustIndentation 606 | } 607 | }, 608 | labelDetailsSupport = true, 609 | }, 610 | contextSupport = true, 611 | insertTextMode = 1, 612 | completionList = { 613 | itemDefaults = { 614 | 'commitCharacters', 615 | 'editRange', 616 | 'insertTextFormat', 617 | 'insertTextMode', 618 | 'data', 619 | } 620 | } 621 | }, 622 | signatureHelp = { 623 | dynamicRegistration = true, 624 | signatureInformation = { 625 | documentationFormat = { 'markdown', 'plaintext' }, 626 | parameterInformation = { 627 | labelOffsetSupport = true, 628 | }, 629 | activeParameterSupport = true, 630 | }, 631 | contextSupport = true, 632 | } 633 | }, 634 | } --[[@as cmp-kit.kit.LSP.ClientCapabilities]] 635 | end 636 | 637 | return ix 638 | -------------------------------------------------------------------------------- /lua/ix/misc.lua: -------------------------------------------------------------------------------- 1 | local kit = require('cmp-kit.kit') 2 | 3 | local misc = {} 4 | 5 | local group = vim.api.nvim_create_augroup('ix', { 6 | clear = true 7 | }) 8 | 9 | ---Create disposable autocmd. 10 | ---@param e string|string[] 11 | ---@param opts vim.api.keyset.create_autocmd 12 | ---@return fun() 13 | function misc.autocmd(e, opts) 14 | local id = vim.api.nvim_create_autocmd(e, kit.merge(opts, { 15 | group = group 16 | })) 17 | return function() 18 | pcall(vim.api.nvim_del_autocmd, id) 19 | end 20 | end 21 | 22 | function misc.autocmd_queue() 23 | local scheduling = false 24 | local queue = {} 25 | return { 26 | add = function(task) 27 | table.insert(queue, task) 28 | if not scheduling then 29 | vim.api.nvim_create_autocmd('SafeState', { 30 | once = true, 31 | callback = function() 32 | local target = queue[#queue] 33 | queue = {} 34 | scheduling = false 35 | if target then 36 | target() 37 | end 38 | end 39 | }) 40 | end 41 | end 42 | } 43 | end 44 | 45 | return misc 46 | -------------------------------------------------------------------------------- /lua/ix/source.lua: -------------------------------------------------------------------------------- 1 | local misc = require('ix.misc') 2 | local kit = require('cmp-kit.kit') 3 | 4 | local source = {} 5 | 6 | source.completion = {} 7 | 8 | ---Create buffer source. 9 | ---@param option? cmp-kit.completion.ext.source.buffer.Option 10 | ---@return cmp-kit.completion.CompletionSource 11 | function source.completion.buffer(option) 12 | return require('cmp-kit.completion.ext.source.buffer')(kit.merge(option or {}, { 13 | gather_keyword_length = 3, 14 | label_details = { 15 | description = 'buffer' 16 | } 17 | } --[[@as cmp-kit.completion.ext.source.buffer.Option]])) 18 | end 19 | 20 | ---Create path source. 21 | ---@param option? cmp-kit.completion.ext.source.path.Option 22 | ---@return cmp-kit.completion.CompletionSource 23 | function source.completion.path(option) 24 | return require('cmp-kit.completion.ext.source.path')(kit.merge(option or {}, { 25 | enable_file_document = true, 26 | } --[[@as cmp-kit.completion.ext.source.path.Option]])) 27 | end 28 | 29 | ---Create calc source. 30 | ---@return cmp-kit.completion.CompletionSource 31 | function source.completion.calc() 32 | return require('cmp-kit.completion.ext.source.calc')() 33 | end 34 | 35 | ---Create cmdline source. 36 | ---@return cmp-kit.completion.CompletionSource 37 | function source.completion.cmdline() 38 | return require('cmp-kit.completion.ext.source.cmdline')() 39 | end 40 | 41 | ---Attach lsp completion source to the completion service. 42 | ---@param completion_service cmp-kit.completion.CompletionService 43 | ---@param option? { bufnr: integer?, group: integer?, priority: integer?, server?: table } 44 | function source.completion.attach_lsp(completion_service, option) 45 | option = option or {} 46 | option.bufnr = option.bufnr or vim.api.nvim_get_current_buf() 47 | option.bufnr = option.bufnr ~= 0 and vim.api.nvim_get_current_buf() or option.bufnr 48 | option.group = option.group or 10 49 | option.priority = option.priority or 100 50 | option.server = option.server or {} 51 | 52 | local attached = {} --[[@type table]] 53 | 54 | -- attach. 55 | local function attach() 56 | for _, client in ipairs(vim.lsp.get_clients({ bufnr = option.bufnr })) do 57 | if attached[client.id] then 58 | attached[client.id]() 59 | end 60 | attached[client.id] = completion_service:register_source( 61 | require('cmp-kit.completion.ext.source.lsp.completion')( 62 | kit.merge({ client = client }, option.server[client.name] or {}) 63 | ), 64 | { 65 | group = option.group, 66 | priority = option.priority 67 | } 68 | ) 69 | end 70 | end 71 | completion_service:on_dispose(misc.autocmd('LspAttach', { 72 | callback = attach 73 | })) 74 | attach() 75 | 76 | -- detach. 77 | completion_service:on_dispose(misc.autocmd('LspDetach', { 78 | callback = function(e) 79 | if attached[e.data.client_id] then 80 | attached[e.data.client_id]() 81 | attached[e.data.client_id] = nil 82 | end 83 | end 84 | })) 85 | end 86 | 87 | source.signature_help = {} 88 | 89 | ---Attach lsp signature_help source to the signature_help service. 90 | ---@param signature_help_service cmp-kit.signature_help.SignatureHelpService 91 | ---@param option? { bufnr: integer?, group: integer?, priority: integer? } 92 | function source.signature_help.attach_lsp(signature_help_service, option) 93 | option = option or {} 94 | option.bufnr = option.bufnr or vim.api.nvim_get_current_buf() 95 | option.bufnr = option.bufnr ~= 0 and vim.api.nvim_get_current_buf() or option.bufnr 96 | option.group = option.group or 10 97 | option.priority = option.priority or 100 98 | 99 | local attached = {} --[[@type table]] 100 | 101 | -- attach. 102 | local function attach() 103 | for _, client in ipairs(vim.lsp.get_clients({ bufnr = option.bufnr })) do 104 | if attached[client.id] then 105 | attached[client.id]() 106 | attached[client.id] = nil 107 | end 108 | attached[client.id] = signature_help_service:register_source( 109 | require('cmp-kit.signature_help.ext.source.lsp.signature_help')({ client = client }), 110 | { 111 | group = option.group, 112 | priority = option.priority 113 | } 114 | ) 115 | end 116 | end 117 | signature_help_service:on_dispose(misc.autocmd('LspAttach', { 118 | callback = attach 119 | })) 120 | attach() 121 | 122 | -- detach. 123 | signature_help_service:on_dispose(misc.autocmd('LspDetach', { 124 | callback = function(e) 125 | if attached[e.data.client_id] then 126 | attached[e.data.client_id]() 127 | attached[e.data.client_id] = nil 128 | end 129 | end 130 | })) 131 | end 132 | 133 | return source 134 | --------------------------------------------------------------------------------