summaryrefslogtreecommitdiff
path: root/lua/blink/cmp/lib/buffer_events.lua
blob: dcca8b8a3b0907b0414958af853299d3fb80081d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
--- Exposes three events (cursor moved, char added, insert leave) for triggers to use.
--- Notably, when "char added" is fired, the "cursor moved" event will not be fired.
--- Unlike in regular neovim, ctrl + c and buffer switching will trigger "insert leave"

--- @class blink.cmp.BufferEvents
--- @field has_context fun(): boolean
--- @field show_in_snippet boolean
--- @field ignore_next_text_changed boolean
--- @field ignore_next_cursor_moved boolean
---
--- @field new fun(opts: blink.cmp.BufferEventsOptions): blink.cmp.BufferEvents
--- @field listen fun(self: blink.cmp.BufferEvents, opts: blink.cmp.BufferEventsListener)
--- @field suppress_events_for_callback fun(self: blink.cmp.BufferEvents, cb: fun())

--- @class blink.cmp.BufferEventsOptions
--- @field has_context fun(): boolean
--- @field show_in_snippet boolean

--- @class blink.cmp.BufferEventsListener
--- @field on_char_added fun(char: string, is_ignored: boolean)
--- @field on_cursor_moved fun(event: 'CursorMoved' | 'InsertEnter', is_ignored: boolean)
--- @field on_insert_leave fun()

--- @type blink.cmp.BufferEvents
--- @diagnostic disable-next-line: missing-fields
local buffer_events = {}

function buffer_events.new(opts)
  return setmetatable({
    has_context = opts.has_context,
    show_in_snippet = opts.show_in_snippet,
    ignore_next_text_changed = false,
    ignore_next_cursor_moved = false,
  }, { __index = buffer_events })
end

--- Normalizes the autocmds + ctrl+c into a common api and handles ignored events
function buffer_events:listen(opts)
  local snippet = require('blink.cmp.config').snippets

  local last_char = ''
  vim.api.nvim_create_autocmd('InsertCharPre', {
    callback = function()
      if snippet.active() and not self.show_in_snippet and not self.has_context() then return end
      last_char = vim.v.char
    end,
  })

  vim.api.nvim_create_autocmd('TextChangedI', {
    callback = function()
      if not require('blink.cmp.config').enabled() then return end
      if snippet.active() and not self.show_in_snippet and not self.has_context() then return end

      local is_ignored = self.ignore_next_text_changed
      self.ignore_next_text_changed = false

      -- no characters added so let cursormoved handle it
      if last_char == '' then return end

      opts.on_char_added(last_char, is_ignored)

      last_char = ''
    end,
  })

  vim.api.nvim_create_autocmd({ 'CursorMoved', 'CursorMovedI', 'InsertEnter' }, {
    callback = function(ev)
      -- only fire a CursorMoved event (notable not CursorMovedI)
      -- when jumping between tab stops in a snippet while showing the menu
      if
        ev.event == 'CursorMoved'
        and (vim.api.nvim_get_mode().mode ~= 'v' or not self.has_context() or not snippet.active())
      then
        return
      end

      local is_cursor_moved = ev.event == 'CursorMoved' or ev.event == 'CursorMovedI'

      local is_ignored = is_cursor_moved and self.ignore_next_cursor_moved
      if is_cursor_moved then self.ignore_next_cursor_moved = false end

      -- characters added so let textchanged handle it
      if last_char ~= '' then return end

      if not require('blink.cmp.config').enabled() then return end
      if not self.show_in_snippet and not self.has_context() and snippet.active() then return end

      opts.on_cursor_moved(is_cursor_moved and 'CursorMoved' or ev.event, is_ignored)
    end,
  })

  -- definitely leaving the context
  vim.api.nvim_create_autocmd({ 'ModeChanged', 'BufLeave' }, {
    callback = function()
      last_char = ''
      -- HACK: when using vim.snippet.expand, the mode switches from insert -> normal -> visual -> select
      -- so we schedule to ignore the intermediary modes
      -- TODO: deduplicate requests
      vim.schedule(function()
        if not vim.tbl_contains({ 'i', 's' }, vim.api.nvim_get_mode().mode) then opts.on_insert_leave() end
      end)
    end,
  })

  -- ctrl+c doesn't trigger InsertLeave so handle it separately
  local ctrl_c = vim.api.nvim_replace_termcodes('<C-c>', true, true, true)
  vim.on_key(function(key)
    if key == ctrl_c then
      vim.schedule(function()
        local mode = vim.api.nvim_get_mode().mode
        if mode ~= 'i' then
          last_char = ''
          opts.on_insert_leave()
        end
      end)
    end
  end)
end

--- Suppresses autocmd events for the duration of the callback
--- HACK: there's likely edge cases with this since we can't know for sure
--- if the autocmds will fire for cursor_moved afaik
function buffer_events:suppress_events_for_callback(cb)
  local cursor_before = vim.api.nvim_win_get_cursor(0)
  local changed_tick_before = vim.api.nvim_buf_get_changedtick(0)

  cb()

  local cursor_after = vim.api.nvim_win_get_cursor(0)
  local changed_tick_after = vim.api.nvim_buf_get_changedtick(0)

  local is_insert_mode = vim.api.nvim_get_mode().mode == 'i'

  self.ignore_next_text_changed = changed_tick_after ~= changed_tick_before and is_insert_mode

  -- HACK: the cursor may move from position (1, 1) to (1, 0) and back to (1, 1) during the callback
  -- This will trigger a CursorMovedI event, but we can't detect it simply by checking the cursor position
  -- since they're equal before vs after the callback. So instead, we always mark the cursor as ignored in
  -- insert mode, but if the cursor was equal, we undo the ignore after a small delay, which practically guarantees
  -- that the CursorMovedI event will fire
  -- TODO: It could make sense to override the nvim_win_set_cursor function and mark as ignored if it's called
  -- on the current buffer
  local cursor_moved = cursor_after[1] ~= cursor_before[1] or cursor_after[2] ~= cursor_before[2]
  self.ignore_next_cursor_moved = is_insert_mode
  if not cursor_moved then vim.defer_fn(function() self.ignore_next_cursor_moved = false end, 10) end
end

return buffer_events