├── 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 |
6 |
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 |
--------------------------------------------------------------------------------