├── lua └── torch │ ├── preset │ ├── keymap.lua │ └── source.lua │ ├── misc.lua │ └── init.lua └── README.md /lua/torch/preset/keymap.lua: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lua/torch/misc.lua: -------------------------------------------------------------------------------- 1 | local kit = require('cmp-kit.kit') 2 | 3 | local misc = {} 4 | 5 | local group = vim.api.nvim_create_augroup('torch', { 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 | return misc 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nvim-torch 2 | 3 | A Neovim completion plugin built on top of 4 | [nvim-cmp-kit](https://github.com/hrsh7th/nvim-cmp-kit). 5 | 6 | This plugin is currently in **beta** and includes built-in sources for lsp, 7 | path, buffer, and cmdline. 8 | 9 | The [nvim-torch](https://github.com/hrsh7th/nvim-torch) is expected to operate 10 | stably, but its customizability currently falls short compared to nvim-cmp. 11 | 12 | If you early-adapter, please try it out and provide feedback. 13 | 14 | ## Why a new completion engine? 15 | 16 | I developed a completion engine called 17 | [nvim-cmp](https://github.com/hrsh7th/nvim-cmp), which I believe was a success. 18 | However, it had several issues: 19 | 20 | - It was difficult to meet the diverse "visual" requirements of users. 21 | - The source API lacked reusability. 22 | - An increasing number of outdated implementations made maintenance more 23 | challenging. 24 | 25 | Additionally, as Neovim has introduced numerous powerful APIs in recent years. 26 | 27 | I believe rewriting the engine will enable more stable behavior. 28 | 29 | ## How to migrate? 30 | 31 | Community sources for [nvim-cmp](https://github.com/hrsh7th/nvim-cmp) are not 32 | supported in nvim-torch. 33 | 34 | Therefore, only users relying solely on the lsp, path, buffer, and cmdline 35 | sources can migrate. 36 | 37 | To migrate, simply remove your nvim-cmp configuration and install nvim-torch. 38 | 39 | ## How to install? 40 | 41 | For [lazy.vim](https://github.com/folke/lazy.nvim). 42 | 43 | ```lua 44 | { 45 | 'hrsh7th/nvim-torch', 46 | dependencies = { 47 | 'hrsh7th/nvim-cmp-kit' 48 | }, 49 | version = '*', 50 | config = function() 51 | local torch = require('torch') 52 | 53 | -- for insert-mode completion. 54 | vim.api.nvim_create_autocmd('FileType', { 55 | callback = function() 56 | torch.attach.i(function() 57 | return torch.preset.i({ 58 | expand_snippet = function(snippet) 59 | return vim.fn['vsnip#anonymous'](snippet) 60 | end 61 | }) 62 | end) 63 | end 64 | }) 65 | 66 | -- for cmdline-mode completion. 67 | torch.attach.c(':', function() 68 | return torch.preset.c() 69 | end) 70 | 71 | -- character mapping for completion context. 72 | do 73 | torch.charmap({ 'i', 'c' }, '', function(ctx) 74 | ctx.complete({ force = true }) 75 | end) 76 | torch.charmap('i', '', function(ctx) 77 | local selection = ctx.get_selection() 78 | ctx.commit(selection.index == 0 and 1 or selection.index, { replace = false }) 79 | end) 80 | torch.charmap({ 'i', 'c' }, '', function(ctx) 81 | local selection = ctx.get_selection() 82 | ctx.commit(selection.index == 0 and 1 or selection.index, { replace = true }) 83 | end) 84 | torch.charmap({ 'i', 'c' }, '', function(ctx) 85 | local selection = ctx.get_selection() 86 | ctx.select(selection.index + 1) 87 | end) 88 | torch.charmap({ 'i', 'c' }, '', function(ctx) 89 | local selection = ctx.get_selection() 90 | ctx.select(selection.index - 1) 91 | end) 92 | torch.charmap({ 'i', 'c' }, '', function(ctx) 93 | local selection = ctx.get_selection() 94 | ctx.select(selection.index - 1) 95 | end) 96 | end 97 | end 98 | } 99 | ``` 100 | 101 | ## Why are there separate plugins for nvim-torch and nvim-cmp-kit? 102 | 103 | nvim-cmp combines multiple implementations into a single plugin, including the 104 | UI, configuration, key mappings, and completion engine. 105 | 106 | Because of this structure, rewriting the plugin required rebuilding the entire 107 | completion engine. 108 | 109 | To avoid such situations in the future, we’ve decided to separate the core 110 | completion engine from the user-facing API. This separation ensures that even if 111 | the user-facing API becomes complex, the completion engine remains reusable. 112 | 113 | Additionally, since the completion engine is now standalone, it can be used 114 | independently, much like how nui.nvim is designed. Who knows—this separation 115 | might inspire some exciting innovations within the community! 116 | -------------------------------------------------------------------------------- /lua/torch/preset/source.lua: -------------------------------------------------------------------------------- 1 | local misc = require('torch.misc') 2 | 3 | local preset_source = {} 4 | 5 | ---Create insert mode sources for preset. 6 | ---@class torch.preset.source.InsertModePresetOption 7 | ---@field public disable_providers? { lsp_completion?: true, calc?: true, path?: true, buffer?: true } 8 | ---@param opts? torch.preset.source.InsertModePresetOption 9 | ---@return fun(service: cmp-kit.core.CompletionService): fun()[] 10 | function preset_source.i(opts) 11 | ---@type fun(service: cmp-kit.core.CompletionService): fun()[] 12 | return function(service) 13 | opts = opts or {} 14 | opts.disable_providers = opts.disable_providers or {} 15 | 16 | local bufnr = vim.api.nvim_get_current_buf() 17 | local disposes = {} 18 | 19 | -- calc. 20 | if not opts.disable_providers.calc then 21 | service:register_source(require('cmp-kit.ext.source.calc')(), { 22 | group = 1, 23 | }) 24 | end 25 | 26 | -- path. 27 | if not opts.disable_providers.path then 28 | service:register_source(require('cmp-kit.ext.source.path')(), { 29 | group = 1, 30 | }) 31 | end 32 | 33 | -- lsp.completion. 34 | if not opts.disable_providers.lsp_completion then 35 | local attached = {} --[[@type table]] 36 | -- attach. 37 | local function attach() 38 | for _, client in ipairs(vim.lsp.get_clients({ bufnr = bufnr })) do 39 | if attached[client.id] then 40 | attached[client.id]() 41 | end 42 | attached[client.id] = service:register_source( 43 | require('cmp-kit.ext.source.lsp.completion')({ 44 | client = client --[[@as vim.lsp.Client]], 45 | }), { 46 | group = 10, 47 | priority = 100 48 | }) 49 | end 50 | end 51 | table.insert(disposes, misc.autocmd('InsertEnter', { 52 | callback = attach 53 | })) 54 | table.insert(disposes, misc.autocmd('LspAttach', { 55 | callback = attach 56 | })) 57 | 58 | -- detach. 59 | table.insert(disposes, misc.autocmd('LspDetach', { 60 | callback = function(e) 61 | if attached[e.data.client_id] then 62 | attached[e.data.client_id]() 63 | attached[e.data.client_id] = nil 64 | end 65 | end 66 | })) 67 | end 68 | 69 | -- buffer. 70 | if not opts.disable_providers.buffer then 71 | service:register_source(require('cmp-kit.ext.source.buffer')({ 72 | min_keyword_length = 3, 73 | label_details = { 74 | description = 'buffer' 75 | } 76 | }), { 77 | group = 100, 78 | dedup = true, 79 | }) 80 | end 81 | 82 | return disposes 83 | end 84 | end 85 | 86 | ---Create cmdline mode sources for preset. 87 | ---@class torch.preset.source.CmdlineModePresetOption 88 | ---@field public disable_providers? { cmdline?: true, calc?: true, path?: true, buffer?: true } 89 | ---@param opts? torch.preset.source.CmdlineModePresetOption 90 | ---@return fun(service: cmp-kit.core.CompletionService): fun()[] 91 | function preset_source.c(opts) 92 | ---@type fun(service: cmp-kit.core.CompletionService): fun()[] 93 | return function(service) 94 | opts = opts or {} 95 | opts.disable_providers = opts.disable_providers or {} 96 | 97 | -- calc. 98 | if not opts.disable_providers.calc then 99 | service:register_source(require('cmp-kit.ext.source.calc')(), { 100 | group = 1, 101 | }) 102 | end 103 | 104 | -- path. 105 | if not opts.disable_providers.path then 106 | service:register_source(require('cmp-kit.ext.source.path')(), { 107 | group = 1, 108 | }) 109 | end 110 | 111 | -- cmdline. 112 | if not opts.disable_providers.cmdline then 113 | service:register_source(require('cmp-kit.ext.source.cmdline')(), { 114 | group = 10, 115 | }) 116 | end 117 | 118 | -- buffer. 119 | if not opts.disable_providers.buffer then 120 | service:register_source(require('cmp-kit.ext.source.buffer')({ 121 | min_keyword_length = 3, 122 | }), { 123 | group = 100, 124 | dedup = true, 125 | }) 126 | end 127 | 128 | return {} 129 | end 130 | end 131 | 132 | return preset_source 133 | -------------------------------------------------------------------------------- /lua/torch/init.lua: -------------------------------------------------------------------------------- 1 | local kit = require('cmp-kit.kit') 2 | local Async = require('cmp-kit.kit.Async') 3 | local Keymap = require('cmp-kit.kit.Vim.Keymap') 4 | local CompletionService = require('cmp-kit.core.CompletionService') 5 | local misc = require('torch.misc') 6 | 7 | ---@class torch.ServiceRegistration 8 | ---@field public service cmp-kit.core.CompletionService 9 | ---@field public dispose fun() 10 | 11 | ---@class torch.Charmap 12 | ---@field public mode ('i' | 'c')[] 13 | ---@field public char string 14 | ---@field public callback fun(ctx: torch.CharmapContext) 15 | 16 | ---@class torch.CharmapContext 17 | ---@field public schedule fun() 18 | ---@field public feedkeys fun(keys: string, remap?: boolean) 19 | ---@field public prevent fun(callback: fun()) 20 | ---@field public close fun() 21 | ---@field public is_menu_visible fun(): boolean 22 | ---@field public is_docs_visible fun(): boolean 23 | ---@field public complete fun(option?: { force?: boolean }) 24 | ---@field public get_selection fun(): cmp-kit.core.Selection 25 | ---@field public select fun(index: integer, preselect?: boolean) 26 | ---@field public scroll_docs fun(delta: integer) 27 | ---@field public commit fun(index: integer, option?: { replace?: boolean, no_snippet?: boolean }) 28 | ---@field public fallback fun() 29 | 30 | ---@class torch.preset.InsertModeOption 31 | ---@field public expand_snippet? cmp-kit.core.ExpandSnippet 32 | ---@field public sync_mode? fun(): boolean 33 | ---@field public view? cmp-kit.core.View 34 | ---@field public sorter? cmp-kit.core.Sorter 35 | ---@field public matcher? cmp-kit.core.Matcher 36 | ---@field public disable_providers? { lsp_completion?: true, path?: true, buffer?: true } 37 | 38 | ---@class torch.preset.CmdlineModeOption 39 | ---@field public sync_mode? fun(): boolean 40 | ---@field public view? cmp-kit.core.View 41 | ---@field public sorter? cmp-kit.core.Sorter 42 | ---@field public matcher? cmp-kit.core.Matcher 43 | ---@field public disable_providers? { cmdline?: true, path?: true, buffer?: true } 44 | 45 | ---@class torch.Config 46 | ---@field public auto boolean 47 | ---@field public expand_snippet? cmp-kit.core.ExpandSnippet 48 | 49 | local torch = {} 50 | 51 | torch.preset_source = require('torch.preset.source') 52 | torch.preset_keymap = require('torch.preset.keymap') 53 | 54 | local private = { 55 | ---The attached services for buffer. 56 | ---@type table 57 | attached_i = {}, 58 | ---The attached services for cmdtype. 59 | ---@type table 60 | attached_c = {}, 61 | ---Onetime completion. 62 | ---@type torch.ServiceRegistration? 63 | onetime = nil, 64 | ---The charmaps. 65 | ---@type torch.Charmap[] 66 | charmaps = {}, 67 | 68 | ---The config. 69 | config = { 70 | auto = true, 71 | }, 72 | } 73 | 74 | ---Get the current service. 75 | ---@return cmp-kit.core.CompletionService? 76 | local function get_service() 77 | if private.onetime then 78 | return private.onetime.service 79 | end 80 | 81 | if vim.api.nvim_get_mode().mode == 'i' then 82 | local v = private.attached_i[vim.api.nvim_get_current_buf()] 83 | return v and v.service 84 | else 85 | local v = private.attached_c[vim.fn.getcmdtype()] 86 | return v and v.service 87 | end 88 | end 89 | 90 | ---Setup char mapping. 91 | do 92 | vim.on_key(function(_, typed) 93 | if not typed or typed == '' then 94 | return 95 | end 96 | local mode = vim.api.nvim_get_mode().mode 97 | 98 | 99 | -- find charmap. 100 | local charmap = vim.iter(private.charmaps):find(function(charmap) 101 | return vim.tbl_contains(charmap.mode, mode) and vim.fn.keytrans(typed) == vim.fn.keytrans(charmap.char) 102 | end) 103 | if not charmap then 104 | return 105 | end 106 | 107 | -- check service conditions. 108 | local service = get_service() 109 | if not service then 110 | return 111 | end 112 | 113 | -- remove typeahead. 114 | while true do 115 | local c = vim.fn.getcharstr(0) 116 | if c == '' then 117 | break 118 | end 119 | end 120 | 121 | -- create charmap context. 122 | local ctx ---@type torch.CharmapContext 123 | ctx = { 124 | prevent = function(callback) 125 | local resume = service:prevent() 126 | callback() 127 | resume() 128 | end, 129 | schedule = function() 130 | Async.schedule():await() 131 | end, 132 | feedkeys = function(keys, remap) 133 | Keymap.send({ { keys = keys, remap = not not remap } }):await() 134 | end, 135 | close = function() 136 | service:clear() 137 | end, 138 | is_menu_visible = function() 139 | return service:is_menu_visible() 140 | end, 141 | is_docs_visible = function() 142 | return service:is_docs_visible() 143 | end, 144 | get_selection = function() 145 | return service:get_selection() 146 | end, 147 | complete = function(option) 148 | service:complete(option):await() 149 | end, 150 | select = function(index, preselect) 151 | service:select(index, preselect):await() 152 | end, 153 | scroll_docs = function(delta) 154 | service:scroll_docs(delta) 155 | end, 156 | commit = function(index, option) 157 | local match = get_service():get_match_at(index) 158 | if match then 159 | service:commit(match.item, option):await() 160 | else 161 | ctx.fallback() 162 | end 163 | end, 164 | fallback = function() 165 | Keymap.send({ { keys = typed, remap = true } }):await() 166 | end, 167 | } 168 | 169 | Async.run(function() 170 | charmap.callback(ctx) 171 | end) 172 | 173 | return '' 174 | end, vim.api.nvim_create_namespace('torch'), {}) 175 | end 176 | 177 | ---Setup insert-mode. 178 | do 179 | local rev = 0 180 | misc.autocmd('TextChangedI', { 181 | callback = function() 182 | local service = get_service() 183 | if service then 184 | service:complete() 185 | end 186 | end 187 | }) 188 | misc.autocmd('CursorMovedI', { 189 | callback = function() 190 | local service = get_service() 191 | if service and service:is_menu_visible() then 192 | service:complete() 193 | end 194 | end 195 | }) 196 | misc.autocmd('ModeChanged', { 197 | callback = function() 198 | rev = rev + 1 199 | local c = rev 200 | vim.schedule(function() 201 | if c ~= rev then 202 | return 203 | end 204 | if vim.api.nvim_get_mode().mode ~= 'i' then 205 | for _, service_and_dispose in pairs(private.attached_i) do 206 | service_and_dispose.service:clear() 207 | end 208 | end 209 | end) 210 | end 211 | }) 212 | end 213 | 214 | ---Setup cmdline-mode. 215 | do 216 | local rev = 0 217 | misc.autocmd('CmdlineChanged', { 218 | callback = function() 219 | rev = rev + 1 220 | local c = rev 221 | vim.schedule(function() 222 | if c ~= rev then 223 | return 224 | end 225 | if vim.fn.mode(1):sub(1, 1) == 'c' then 226 | local service = get_service() 227 | if service then 228 | service:complete() 229 | end 230 | end 231 | end) 232 | end 233 | }) 234 | misc.autocmd('ModeChanged', { 235 | callback = function() 236 | rev = rev + 1 237 | local c = rev 238 | vim.schedule(function() 239 | if c ~= rev then 240 | return 241 | end 242 | local is_not_cmdline = vim.api.nvim_get_mode().mode ~= 'c' 243 | for cmdtype, service_and_dispose in pairs(private.attached_c) do 244 | if is_not_cmdline or vim.fn.getcmdtype() ~= cmdtype then 245 | service_and_dispose.service:clear() 246 | end 247 | end 248 | end) 249 | end 250 | }) 251 | end 252 | 253 | ---Setup. 254 | ---@param config torch.Config|{} 255 | function torch.setup(config) 256 | private.config = kit.merge(config, private.config) 257 | end 258 | 259 | torch.attach = {} 260 | 261 | ---Attach a service to buffer. 262 | ---@param setup fun(service: cmp-kit.core.CompletionService): fun()[] 263 | function torch.attach.i(setup) 264 | local bufnr = vim.api.nvim_get_current_buf() 265 | local attached = private.attached_i[bufnr] 266 | if attached then 267 | attached.dispose() 268 | end 269 | 270 | local service = CompletionService.new({ 271 | expand_snippet = private.config.expand_snippet, 272 | }) 273 | local disposes = setup(service) 274 | private.attached_i[bufnr] = { 275 | service = service, 276 | dispose = function() 277 | for _, dispose in ipairs(disposes) do 278 | dispose() 279 | end 280 | end, 281 | } 282 | end 283 | 284 | ---Attach a service to cmdtype. 285 | ---@param setup fun(service: cmp-kit.core.CompletionService): fun()[] 286 | function torch.attach.c(cmdtype, setup) 287 | local attached = private.attached_i[cmdtype] 288 | if attached then 289 | attached.dispose() 290 | end 291 | 292 | local service = CompletionService.new({}) 293 | local disposes = setup(service) 294 | private.attached_c[cmdtype] = { 295 | service = service, 296 | dispose = function() 297 | for _, dispose in ipairs(disposes) do 298 | dispose() 299 | end 300 | end, 301 | } 302 | end 303 | 304 | ---Do onetime completion. 305 | ---@param option { force?: boolean } 306 | ---@param setup fun(service: cmp-kit.core.CompletionService): fun()[] 307 | function torch.onetime(option, setup) 308 | local current_service = get_service() 309 | if current_service then 310 | current_service:clear() 311 | end 312 | 313 | local service = CompletionService.new({}) 314 | local disposes = setup(service) 315 | private.onetime = { 316 | service = service, 317 | dispose = function() 318 | for _, dispose in ipairs(disposes) do 319 | dispose() 320 | end 321 | service:dispose() 322 | private.onetime = nil 323 | end, 324 | } 325 | 326 | service:complete(option):next(function() 327 | if service:is_menu_visible() then 328 | service:on_menu_hide(function() 329 | private.onetime.dispose() 330 | end) 331 | else 332 | private.onetime.dispose() 333 | end 334 | end) 335 | end 336 | 337 | ---Setup character mapping. 338 | ---@param mode 'i' | 'c' | ('i' | 'c')[] 339 | ---@param char string 340 | ---@param callback fun(ctx: torch.CharmapContext) 341 | function torch.charmap(mode, char, callback) 342 | local l = 0 343 | local i = 1 344 | local n = false 345 | while i <= #char do 346 | local c = char:sub(i, i) 347 | if c == '<' then 348 | n = true 349 | elseif c == '\\' then 350 | i = i + 1 351 | else 352 | if n then 353 | if c == '>' then 354 | n = false 355 | l = l + 1 356 | end 357 | else 358 | l = l + 1 359 | end 360 | end 361 | i = i + 1 362 | end 363 | 364 | if l > 1 then 365 | error('multiple key sequence is not supported') 366 | end 367 | 368 | table.insert(private.charmaps, { 369 | mode = kit.to_array(mode), 370 | char = vim.keycode(char), 371 | callback = callback, 372 | }) 373 | end 374 | 375 | return torch 376 | --------------------------------------------------------------------------------