Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: First pass at terminal-mode completions #665

Merged
merged 4 commits into from
Feb 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
- Auto-bracket support based on semantic tokens
- Signature help (experimental, opt-in)
- Command line completion
- [WIP Terminal shell completion](https://github.com/Saghen/blink.cmp/pull/665)
- Terminal completion (although no source for shell completions exists yet, contributions welcome!)
- [Comparison with nvim-cmp](https://cmp.saghen.dev/#compared-to-nvim-cmp)

## Installation
Expand Down
9 changes: 7 additions & 2 deletions docs/configuration/keymap.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,9 @@ keymap = {
'select_next'
},

-- optionally, separate cmdline keymaps
-- optionally, separate cmdline and terminal keymaps
-- cmdline = {}
-- term = {}
}
```

Expand Down Expand Up @@ -62,7 +63,7 @@ keymap = {
- `snippet_backward`: Jumps to the previous snippet placeholder
- `fallback`: Runs the next non-blink keymap, or runs the built-in neovim binding

## Cmdline
## Cmdline and Terminal

You may set a separate keymap for cmdline by defining `keymap.cmdline`, with an identical structure to `keymap`.

Expand All @@ -73,6 +74,10 @@ keymap = {
cmdline = {
preset = 'enter',
...
},
term = {
preset = 'super-tab',
...
}
}
```
Expand Down
3 changes: 3 additions & 0 deletions docs/configuration/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,9 @@ sources = {
return {}
end,

-- By default, we don't enable any terminal sources, but you may try `path` or others
term = {},

-- Function to use when transforming the items before they're returned for all providers
-- The default will lower the score for snippets to sort them lower in the list
transform_items = function(_, items) return items end,
Expand Down
4 changes: 4 additions & 0 deletions docs/configuration/sources.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ sources.providers.lsp = {
}
```

## Terminal and Cmdline Sources

You may use `cmdline` and `term` sources via the `sources.cmdline` and `sources.term` tables. You may see the defaults in the [reference](./reference.md#sources). There's no source for shell completions at the moment, [contributions welcome](https://github.com/Saghen/blink.cmp/issues/1149)!

## Using `nvim-cmp` sources

Blink can use `nvim-cmp` sources through a compatibility layer developed by [stefanboca](https://github.com/stefanboca): [blink.compat](https://github.com/Saghen/blink.compat). Please open any issues with `blink.compat` in that repo
Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
- Auto-bracket support based on semantic tokens
- Signature help (experimental, opt-in)
- Command line completion
- [WIP Terminal shell completion](https://github.com/Saghen/blink.cmp/pull/665)
- Terminal completion (although no source for shell completions exists yet, contributions welcome!)
- [Comparison with nvim-cmp](https://cmp.saghen.dev/#compared-to-nvim-cmp)

## Special Thanks
Expand Down
2 changes: 1 addition & 1 deletion lua/blink/cmp/completion/accept/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ local function accept(ctx, item, callback)
table.insert(all_text_edits, item.textEdit)
text_edits_lib.apply(all_text_edits)

ctx.set_cursor(new_cursor)
if ctx.get_mode() ~= 'term' then ctx.set_cursor(new_cursor) end
end

-- Let the source execute the item itself
Expand Down
6 changes: 5 additions & 1 deletion lua/blink/cmp/completion/trigger/context.lua
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,10 @@ function context:within_query_bounds(cursor)
return row == bounds.line_number and col >= bounds.start_col and col < (bounds.start_col + bounds.length)
end

function context.get_mode() return vim.api.nvim_get_mode().mode == 'c' and 'cmdline' or 'default' end
function context.get_mode()
local mode = vim.api.nvim_get_mode().mode
return (mode == 'c' and 'cmdline') or (mode == 't' and 'term') or 'default'
end

function context.get_cursor()
return context.get_mode() == 'cmdline' and { 1, vim.fn.getcmdpos() - 1 } or vim.api.nvim_win_get_cursor(0)
Expand All @@ -106,6 +109,7 @@ function context.get_line(num)
return vim.fn.getcmdline()
end

-- This method works for normal buffers and the terminal prompt
if num == nil then num = context.get_cursor()[1] - 1 end
return vim.api.nvim_buf_get_lines(0, num, num + 1, false)[1]
end
Expand Down
40 changes: 28 additions & 12 deletions lua/blink/cmp/completion/trigger/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
--- @class blink.cmp.CompletionTrigger
--- @field buffer_events blink.cmp.BufferEvents
--- @field cmdline_events blink.cmp.CmdlineEvents
--- @field term_events blink.cmp.TermEvents
--- @field current_context_id number
--- @field context? blink.cmp.Context
--- @field show_emitter blink.cmp.EventEmitter<{ context: blink.cmp.Context }>
Expand Down Expand Up @@ -48,22 +49,24 @@ local function on_char_added(char, is_ignored)
if is_ignored then
if trigger.context ~= nil then trigger.show({ send_upstream = false, trigger_kind = 'keyword' }) end

-- character forces a trigger according to the sources, create a fresh context
-- character forces a trigger according to the sources, create a fresh context
elseif trigger.is_trigger_character(char) and (config.show_on_trigger_character or trigger.context ~= nil) then
trigger.context = nil
trigger.show({ trigger_kind = 'trigger_character', trigger_character = char })

-- character is part of a keyword
-- character is part of a keyword
elseif fuzzy.is_keyword_character(char) and (config.show_on_keyword or trigger.context ~= nil) then
trigger.show({ trigger_kind = 'keyword' })

-- nothing matches so hide
-- nothing matches so hide
else
trigger.hide()
end
end

local function on_cursor_moved(event, is_ignored)
local is_enter_event = event == 'InsertEnter' or event == 'TermEnter'

local cursor = context.get_cursor()
local cursor_col = cursor[2]

Expand Down Expand Up @@ -91,32 +94,32 @@ local function on_cursor_moved(event, is_ignored)
-- Reproducible with `example.|a` and pressing `a`, should not show the menu
local insert_enter_on_trigger_character = config.show_on_trigger_character
and config.show_on_insert_on_trigger_character
and event == 'InsertEnter'
and is_enter_event
and trigger.is_trigger_character(char_under_cursor, true)

-- check if we're still within the bounds of the query used for the context
if trigger.context ~= nil and trigger.context:within_query_bounds(cursor) then
trigger.show({ trigger_kind = 'keyword' })

-- check if we've entered insert mode on a trigger character
-- or if we've moved onto a trigger character while open
-- check if we've entered insert mode on a trigger character
-- or if we've moved onto a trigger character while open
elseif
insert_enter_on_trigger_character
or (is_on_trigger_for_show and trigger.context ~= nil and trigger.context.trigger.kind ~= 'prefetch')
then
trigger.context = nil
trigger.show({ trigger_kind = 'trigger_character', trigger_character = char_under_cursor })

-- show if we currently have a context, and we've moved outside of it's bounds by 1 char
-- show if we currently have a context, and we've moved outside of it's bounds by 1 char
elseif is_keyword and trigger.context ~= nil and cursor_col == trigger.context.bounds.start_col - 1 then
trigger.context = nil
trigger.show({ trigger_kind = 'keyword' })

-- prefetch completions without opening window on InsertEnter
elseif event == 'InsertEnter' and config.prefetch_on_insert then
-- prefetch completions without opening window on InsertEnter
elseif is_enter_event and config.prefetch_on_insert then
trigger.show({ trigger_kind = 'prefetch' })

-- otherwise hide
-- otherwise hide
else
trigger.hide()
end
Expand All @@ -128,6 +131,7 @@ function trigger.activate()
has_context = function() return trigger.context ~= nil end,
show_in_snippet = config.show_in_snippet,
})

trigger.buffer_events:listen({
on_char_added = on_char_added,
on_cursor_moved = on_cursor_moved,
Expand All @@ -140,6 +144,14 @@ function trigger.activate()
on_cursor_moved = on_cursor_moved,
on_leave = function() trigger.hide() end,
})

trigger.term_events = require('blink.cmp.lib.term_events').new({
has_context = function() return trigger.context ~= nil end,
})
trigger.term_events:listen({
on_char_added = on_char_added,
on_term_leave = function() trigger.hide() end,
})
end

function trigger.resubscribe()
Expand Down Expand Up @@ -168,9 +180,13 @@ end

--- Suppresses on_hide and on_show events for the duration of the callback
function trigger.suppress_events_for_callback(cb)
local mode = vim.api.nvim_get_mode().mode == 'c' and 'cmdline' or 'default'
local mode = vim.api.nvim_get_mode().mode
mode = (vim.api.nvim_get_mode().mode == 'c' and 'cmdline') or (mode == 't' and 'term') or 'default'

local events = (mode == 'default' and trigger.buffer_events)
or (mode == 'term' and trigger.term_events)
or trigger.cmdline_events

local events = mode == 'default' and trigger.buffer_events or trigger.cmdline_events
if not events then return cb() end

events:suppress_events_for_callback(cb)
Expand Down
11 changes: 9 additions & 2 deletions lua/blink/cmp/config/keymap.lua
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
--- @alias blink.cmp.KeymapCommand

--- | 'fallback' Fallback to the built-in behavior
--- | 'show' Show the completion window
--- | 'show_and_insert' Show the completion window and select the first item
Expand Down Expand Up @@ -109,6 +110,11 @@
--- cmdline = {
--- preset = 'cmdline',
--- }
---
--- -- optionally, define different keymaps for Neovim's built-in terminal
--- term = {
--- preset = 'term',
--- }
--- }
--- ```
---
Expand All @@ -119,6 +125,7 @@

--- @class (exact) blink.cmp.KeymapConfig : blink.cmp.BaseKeymapConfig
--- @field cmdline? blink.cmp.BaseKeymapConfig Optionally, define a separate keymap for cmdline
--- @field term? blink.cmp.BaseKeymapConfig Optionally, define a separate keymap for cmdline

local keymap = {
--- @type blink.cmp.KeymapConfig
Expand Down Expand Up @@ -152,8 +159,8 @@ function keymap.validate(config)

local validation_schema = {}
for key, value in pairs(config) do
-- nested cmdline keymap
if key == 'cmdline' then
-- nested cmdline/term keymap
if key == 'cmdline' or key == 'term' then
keymap.validate(value)

-- preset
Expand Down
10 changes: 10 additions & 0 deletions lua/blink/cmp/config/sources.lua
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
--- @field default string[] | fun(): string[]
--- @field per_filetype table<string, string[] | fun(): string[]>
--- @field cmdline string[] | fun(): string[]
--- @field term string[] | fun(): string[]
---
--- @field transform_items fun(ctx: blink.cmp.Context, items: blink.cmp.CompletionItem[]): blink.cmp.CompletionItem[] Function to transform the items before they're returned
--- @field min_keyword_length number | fun(ctx: blink.cmp.Context): number Minimum number of characters in the keyword to trigger
Expand Down Expand Up @@ -53,6 +54,7 @@ local sources = {
if type == ':' or type == '@' then return { 'cmdline' } end
return {}
end,
term = {},

transform_items = function(_, items) return items end,
min_keyword_length = 0,
Expand Down Expand Up @@ -101,6 +103,13 @@ local sources = {
name = 'Omni',
module = 'blink.cmp.sources.omni',
},
-- NOTE: in future we may want a built-in terminal source. For now
-- the infrastructure exists, e.g. so community terminal sources can be
-- added, but this functionality is not baked into blink.cmp.
-- term = {
-- name = 'term',
-- module = 'blink.cmp.sources.term',
-- },
},
},
}
Expand All @@ -115,6 +124,7 @@ function sources.validate(config)
default = { config.default, { 'function', 'table' } },
per_filetype = { config.per_filetype, 'table' },
cmdline = { config.cmdline, { 'function', 'table' } },
term = { config.term, { 'function', 'table' } },

transform_items = { config.transform_items, 'function' },
min_keyword_length = { config.min_keyword_length, { 'number', 'function' } },
Expand Down
38 changes: 35 additions & 3 deletions lua/blink/cmp/keymap/apply.lua
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ function apply.keymap_to_current_buffer(keys_to_commands)
if command == 'fallback' then
return fallback()

-- run user defined functions
-- run user defined functions
elseif type(command) == 'function' then
if command(require('blink.cmp')) then return end

-- otherwise, run the built-in command
-- otherwise, run the built-in command
elseif require('blink.cmp')[command]() then
return
end
Expand Down Expand Up @@ -70,6 +70,38 @@ function apply.keymap_to_current_buffer(keys_to_commands)
end
end

function apply.term_keymaps(keys_to_commands)
-- skip if we've already applied the keymaps
for _, mapping in ipairs(vim.api.nvim_buf_get_keymap(0, 't')) do
if mapping.desc == 'blink.cmp' then return end
end

-- terminal mode: uses insert commands only
for key, commands in pairs(keys_to_commands) do
if #commands == 0 then goto continue end

local fallback = require('blink.cmp.keymap.fallback').wrap('i', key)
apply.set('t', key, function()
for _, command in ipairs(commands) do
-- special case for fallback
if command == 'fallback' then
return fallback()

-- run user defined functions
elseif type(command) == 'function' then
if command(require('blink.cmp')) then return end

-- otherwise, run the built-in command
elseif require('blink.cmp')[command]() then
return
end
end
end)

::continue::
end
end

function apply.cmdline_keymaps(keys_to_commands)
-- cmdline mode: uses only insert commands
for key, commands in pairs(keys_to_commands) do
Expand Down Expand Up @@ -106,7 +138,7 @@ end
--- @param key string
--- @param callback fun(): string | nil
function apply.set(mode, key, callback)
if mode == 'c' then
if mode == 'c' or mode == 't' then
vim.api.nvim_set_keymap(mode, key, '', {
callback = callback,
expr = true,
Expand Down
8 changes: 8 additions & 0 deletions lua/blink/cmp/keymap/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,14 @@ function keymap.setup()
local cmdline_mappings = keymap.get_mappings(config.keymap.cmdline or config.keymap)
require('blink.cmp.keymap.apply').cmdline_keymaps(cmdline_mappings)
end


-- Apply term keymaps
local term_sources = require('blink.cmp.config').sources.term
if type(term_sources) ~= 'table' or #term_sources > 0 then
local term_mappings = keymap.get_mappings(config.keymap.term or config.keymap)
require('blink.cmp.keymap.apply').term_keymaps(term_mappings)
end
end

return keymap
Loading
Loading