Skip to content

Commit

Permalink
feat: terminal-mode completions (#665)
Browse files Browse the repository at this point in the history
* feat: terminal mode completions

Remove debug line

Use chansend() to set prompt text

Add configuration options for terminal sources and keymaps

Make a note about terminal completions in README (and fix a broken link)

* docs: terminal mode completions

* feat: disable all sources by default for terminal mode

* docs: additional documentation for terminal mode keymaps

---------

Co-authored-by: Liam Dyer <liamcdyer@gmail.com>
  • Loading branch information
wurli and Saghen authored Feb 4, 2025
1 parent 4ac2c27 commit 7b4e546
Show file tree
Hide file tree
Showing 15 changed files with 232 additions and 32 deletions.
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

0 comments on commit 7b4e546

Please sign in to comment.