|
| 1 | +local config = require('blink.cmp.config') |
| 2 | +local sources = { |
| 3 | + registered = { |
| 4 | + lsp = require('blink.cmp.sources.lsp'), |
| 5 | + buffer = require('blink.cmp.sources.buffer'), |
| 6 | + snippets = require('blink.cmp.sources.snippets'), |
| 7 | + }, |
| 8 | + |
| 9 | + -- hack: sweet mother of all hacks |
| 10 | + last_in_flight_id = -1, |
| 11 | + in_flight_id = { |
| 12 | + lsp = -1, |
| 13 | + buffer = -1, |
| 14 | + snippets = -1, |
| 15 | + }, |
| 16 | + |
| 17 | + sources_responses = {}, |
| 18 | + current_context_id = -1, |
| 19 | + on_completions_callback = function(_) end, |
| 20 | +} |
| 21 | + |
| 22 | +--- @return string[] |
| 23 | +function sources.get_trigger_characters() |
| 24 | + local blocked_trigger_characters = {} |
| 25 | + for _, char in ipairs(config.trigger.blocked_trigger_characters) do |
| 26 | + blocked_trigger_characters[char] = true |
| 27 | + end |
| 28 | + |
| 29 | + local trigger_characters = {} |
| 30 | + for _, source in pairs(sources.registered) do |
| 31 | + if source.get_trigger_characters ~= nil then |
| 32 | + local source_trigger_characters = source.get_trigger_characters() |
| 33 | + for _, char in ipairs(source_trigger_characters) do |
| 34 | + if not blocked_trigger_characters[char] then table.insert(trigger_characters, char) end |
| 35 | + end |
| 36 | + end |
| 37 | + end |
| 38 | + return trigger_characters |
| 39 | +end |
| 40 | + |
| 41 | +function sources.listen_on_completions(callback) sources.on_completions_callback = callback end |
| 42 | + |
| 43 | +--- @param context blink.cmp.ShowContext |
| 44 | +function sources.completions(context) |
| 45 | + -- a new context means we should refetch everything |
| 46 | + local is_new_context = context.id ~= sources.current_context_id |
| 47 | + sources.current_context_id = context.id |
| 48 | + |
| 49 | + if is_new_context then sources.sources_responses = {} end |
| 50 | + |
| 51 | + for source_name, source in pairs(sources.registered) do |
| 52 | + -- the source indicates we should refetch when this character is typed |
| 53 | + local trigger_characters = source.get_trigger_characters ~= nil and source.get_trigger_characters() or {} |
| 54 | + local trigger_character = context.trigger_character |
| 55 | + and vim.tbl_contains(trigger_characters, context.trigger_character) |
| 56 | + -- the source indicates the previous results were incomplete and should be refetched on the next trigger |
| 57 | + local previous_incomplete = sources.sources_responses[source_name] ~= nil |
| 58 | + and sources.sources_responses[source_name].isIncomplete |
| 59 | + -- check if we have no data and no calls are in flight |
| 60 | + local no_data = sources.sources_responses[source_name] == nil and sources.in_flight_id[source_name] == -1 |
| 61 | + |
| 62 | + -- if none of these are true, we can use the existing cached results |
| 63 | + if is_new_context or trigger_character or previous_incomplete or no_data then |
| 64 | + if source.cancel_completions ~= nil then source.cancel_completions() end |
| 65 | + |
| 66 | + -- register the call |
| 67 | + sources.last_in_flight_id = sources.last_in_flight_id + 1 |
| 68 | + local in_flight_id = sources.last_in_flight_id |
| 69 | + sources.in_flight_id[source_name] = in_flight_id |
| 70 | + |
| 71 | + -- get the reason for the trigger |
| 72 | + local trigger_context = trigger_character |
| 73 | + and { kind = vim.lsp.protocol.CompletionTriggerKind.TriggerCharacter, character = context.trigger_character } |
| 74 | + or previous_incomplete and { kind = vim.lsp.protocol.CompletionTriggerKind.TriggerForIncompleteCompletions } |
| 75 | + or { kind = vim.lsp.protocol.CompletionTriggerKind.Invoked } |
| 76 | + |
| 77 | + -- fetch them completions |
| 78 | + -- fixme: what if we refetch due to incomplete items or a trigger_character? the context trigger id wouldnt change |
| 79 | + -- change so stale data would be returned if the source doesn't support cancellation |
| 80 | + local cursor_column = vim.api.nvim_win_get_cursor(0)[2] |
| 81 | + local source_context = vim.fn.deepcopy(context) |
| 82 | + source_context.trigger = trigger_context |
| 83 | + source.completions(source_context, function(items) |
| 84 | + -- a new call was made or this one was cancelled |
| 85 | + if sources.in_flight_id[source_name] ~= in_flight_id then return end |
| 86 | + sources.in_flight_id[source_name] = -1 |
| 87 | + |
| 88 | + sources.add_source_completions(source_name, items, cursor_column) |
| 89 | + if not sources.some_in_flight() then sources.send_completions(context) end |
| 90 | + end) |
| 91 | + end |
| 92 | + end |
| 93 | + |
| 94 | + -- no completions will be in flight if none of them ran, |
| 95 | + -- so we send the completions |
| 96 | + if not sources.some_in_flight() then sources.send_completions(context) end |
| 97 | +end |
| 98 | + |
| 99 | +--- @param source_name string |
| 100 | +--- @param source_response blink.cmp.CompletionResponse |
| 101 | +--- @param cursor_column number |
| 102 | +function sources.add_source_completions(source_name, source_response, cursor_column) |
| 103 | + for _, item in ipairs(source_response.items) do |
| 104 | + item.source = source_name |
| 105 | + item.cursor_column = cursor_column |
| 106 | + end |
| 107 | + |
| 108 | + sources.sources_responses[source_name] = source_response |
| 109 | +end |
| 110 | + |
| 111 | +--- @return boolean |
| 112 | +function sources.some_in_flight() |
| 113 | + for _, in_flight in pairs(sources.in_flight_id) do |
| 114 | + if in_flight ~= -1 then return true end |
| 115 | + end |
| 116 | + return false |
| 117 | +end |
| 118 | + |
| 119 | +--- @param context blink.cmp.ShowContext |
| 120 | +function sources.send_completions(context) |
| 121 | + local sources_responses = sources.sources_responses |
| 122 | + -- apply source filters |
| 123 | + for _, source in pairs(sources.registered) do |
| 124 | + if source.filter_completions ~= nil then |
| 125 | + sources_responses = source.filter_completions(context, sources_responses) |
| 126 | + end |
| 127 | + end |
| 128 | + |
| 129 | + -- flatten the items |
| 130 | + local flattened_items = {} |
| 131 | + for source_name, response in pairs(sources.sources_responses) do |
| 132 | + local source = sources.registered[source_name] |
| 133 | + if source.should_show_completions == nil or source.should_show_completions(context, sources_responses) then |
| 134 | + vim.list_extend(flattened_items, response.items) |
| 135 | + end |
| 136 | + end |
| 137 | + |
| 138 | + sources.on_completions_callback(context, flattened_items) |
| 139 | +end |
| 140 | + |
| 141 | +function sources.cancel_completions() |
| 142 | + for source_name, source in pairs(sources.registered) do |
| 143 | + sources.in_flight_id[source_name] = -1 |
| 144 | + if source.cancel_completions ~= nil then source.cancel_completions() end |
| 145 | + end |
| 146 | +end |
| 147 | + |
| 148 | +--- @param item blink.cmp.CompletionItem |
| 149 | +--- @param callback fun(resolved_item: blink.cmp.CompletionItem | nil) |
| 150 | +--- @return fun(): nil Cancelation function |
| 151 | +function sources.resolve(item, callback) |
| 152 | + local item_source = sources.registered[item.source] |
| 153 | + if item_source == nil or item_source.resolve == nil then |
| 154 | + callback(nil) |
| 155 | + return function() end |
| 156 | + end |
| 157 | + return item_source.resolve(item, callback) or function() end |
| 158 | +end |
| 159 | + |
| 160 | +return sources |
0 commit comments